8 ms·
On F# code readability
- crntaylor 14y agoI disagree with his example of 'unintelligible' code. He suggests replacing match list with | x::xs -> ... with match list with | head::tail -> ... to make it clear what the names are referring to. But in F#, something to the left of :: can never be anything other than the head of a list, and something to the right of :: can never be anything other than the tail of a list! Calling them 'head' and 'tail' doesn't add any extra information. It just adds more characters. The names x and xs are entirely appropriate when you don't know anything about the values you're working with (not even their types). All you know is that x has a type 'a, and xs has the type 'a list. Giving them abstract names reflects the fact that you are writing an abstract function. If you are writing a more concrete function, use more concrete names (e.g. trade or blogPost or playerAction... but not head and tail!) Also, I don't know any F#, but is he correct when he says that the order of declarations in a file matters, and that the order of files in a project matters? That sounds crazy to me. What is the benefit? Both Haskell and Ocaml get along fine without relying on declaration order to do type inference. Other than that - good article, I agree!
- sdevlin 14y ago> Also, I don't know any F#, but is he correct when he says that the order of declarations in a file matters, and that the order of files in a project matters? He is correct. (At least, this was true a couple years ago. I haven't used F# since then, but I doubt this has changed.) I don't have enough F# experience to speak to any potential benefits of this restriction.
- thomasz 14y agoIt's a pain in the ass, and the only benefit I can think of is that it made the compiler easier to implement.
- snprbob86 14y agoMany languages require declare-before-use and co-declared recursive structures. Sometimes, they provide an escape-hatch like a forward-declaration. It also makes the compiler substantially faster and makes it far harder to accidentally introduce a layering violation in your design. If you're a Java or C# programmer, or even a C++ programmer, you're used to cyclic relationships all over the place. Your Person class has a .Adresses property and your Address class has a .Residents property so that you can conveniently person.Addresses.Filter(address => address.Residents.Count > 1) and suddenly your Address class is tightly coupled to your Person class. Do this a few dozen times and before you know it, your software is a maintenance and refactoring nightmare. Object-oriented programming encourages this design. Functional programming discourages it. Modern OOP compilers conveniently resolve circular dependencies (Java does, C++ doesn't without header games). Modern FP compilers force you to declare and contain your circular dependencies. Lastly, declare-before-use makes a lot of dynamic and live programming scenarios viable. Lisps, for example, generally have a compilation unit of a top-level form. This way the REPL and a normal source file behave more similarly, if not precisely the same.
- thomasz 14y agoI'm totally fine mandating explicit declarations for mutual dependencies, I'm just pissed that F# has no syntax for dependent modules. It's just odd. Why can I declare co-dependent functions and types, but not modules?
- snprbob86 14y agoIf they are co-dependent, then that, by definition, means that they are not independent. What value do you get by having co-dependent modules that you can't get by having a single module? If you want two public APIs with one common implementation that is co-dependent, you can have three modules where two depend on the co-dependent part. Without seeing a motivating use-case, I'm just going to assume that F# is succeeding in preventing you from making a bad design decision.
- watt 14y agoOh but it does.... I don't know first thing about F#, so "x::xs" looks alien and very unapproachable (and also will make the rest of method body confusing). In my mind it's that code readability is at last being discussed, signals that F# might be nearing mainstream.
- sdevlin 14y agoSurely questions of readability should be decided with an eye towards experienced users of the language and not those who "don't know the first thing about F#". I agree with the GP that x::xs is more appropriate.
- watt 14y agoYou will end up with just another fringe write-only language.
- chc 14y agoI do not think that phrase means what you think it means. If the code is readable to somebody of average skill who has learned the language, it is not "write-only." The fact that you can't read code in a language you don't know shouldn't surprise you any more than the fact that I can't read a book in Vietnamese. My inability to read Vietnamese does not make it "write-only" — it's just a sign that I haven't learned the language. As for "fringe," that may or may not be true. It's pretty subjective. But really, who cares? Back when Ruby was the weird new thing gaining visibility, I heard the same complaints about its idioms. Last I checked, it's still doing fine. I'm OK using "fringe" languages like Ruby.
- abraininavat 14y agoIs readability to people who don't know the first thing about F# at all relevant? If that's the most important metric then we should write out all our programs in plain English. Readability to F# programmers is what matters, along with a host of other concerns, such as terseness.
- 14y ago
- kvb 14y agoI agree that using head::tail is usually not more readable, though I think that sometimes it is better to be a bit more descriptive than just x::xs (e.g. key::keys). It really depends on the context as well as the experience level of the people who will be reading the code. It is true that the order of files matters. Presumably this is for the same reason that order matters within a file.
- mich41 14y ago> Also, I don't know any F#, but is he correct when he says that the order of declarations in a file matters, and that the order of files in a project matters? That sounds crazy to me. What is the benefit? Both Haskell and Ocaml get along fine without relying on declaration order to do type inference. IIRC in OCaml you have to compile modules in topological order wrt dependencies.
- sitharus 14y agoYes, order of files in a project matters. If Things.fs contains type Foo and Main.fs uses it, then the project must look like MyProj |- Things.fs |- Main.fs Visual Studio has a few functions to manage this order. The benefit is faster compilation, because F# requires recursive types to be declared together, such as type Foo = A of Bar | B and Bar = {Baz: Foo} this doesn't really come up that much.
- lysium 14y ago> this doesn't really come up that much. Unless, of course, if you don't know or expect that 'rule' and you are wondering, why your type is not visible in that other module for over an hour...
- gtani 14y agoIf you were writing for haskell/scala/ML'ers, x::xs, for C#ers, head::tail I haven't been tracking closely but no Forward declarations was one of common complaints, along with organizing projects in subdirectoris (I think they have fixes for that now) and VS support in general http://stackoverflow.com/questions/1378575/f-forward-type-declarations http://stackoverflow.com/questions/1378575/f-forward-type-de... __________ Also Xamarin shd be releasing... soon http://news.ycombinator.com/item?id=5251413 http://news.ycombinator.com/item?id=5251413
- Associat0r 14y ago> Also, I don't know any F#, but is he correct when he says that the order of declarations in a file matters, and that the order of files in a project matters? > That sounds crazy to me. What is the benefit? Both Haskell and Ocaml get along fine without relying on declaration order to do type inference. Here is great explanation on the benefits of this. http://cs.hubfs.net/topic/None/59219#comment-70220 http://cs.hubfs.net/topic/None/59219#comment-70220
- mokus 14y agoI've long felt that very-short names like "x" and "i" should be treated like pronouns in English. They should only be used when the scope is fairly limited and there shouldn't be too many in use at once. There are probably other sensible guidelines I've missed here. When used well they often make code more readable than "descriptive" names, just like most English text is more readable with pronouns than without. In any case, categorically claiming they are bad makes about as much sense to me as banning pronouns from English.
- jamesaguilar 14y agoThat's a really great rule of thumb. I think I might use that. x::xs might not be so nice if the body of the match has twenty lines in it, especially if y::ys also exists. That'd be the pseudo-equivalent of saying, "Joe and Sam were getting food and he wanted a burger but he wanted steak so he finally argued that they should go to Bob's Grill which had both and he agreed." (Not really, because the sentence is actually totally ambiguous whereas with variables it just involves an extra lookup for the reader.) But if the body is one or two lines, there's not much purpose in the extra characters.
- flaie 14y agoYou're right about the fact that on the left on `::` can never be anything other than the head of a list. But I'd like to add that throughout almost all OCaml code I've written and used to read, that type of matching was written: match list with | h::t -> Wich reads easily `h` for `head` and `t` for `tail`. I'm surprised it hasn't been reproduced in the F# world. `h::t` seems even better to me than `head`/`tail` or `x`/`xs` which for the second one, means nothing really to me.
- mich41 14y agoMaybe your code isn't famous enough. I've seen lots of x::xs's everywhere, but never a single h::t. Personally, I always use x::xs in one-liners and something meaningful (no, neither head nor h is meaningful) in longer matches.
- flaie 14y agoI've never mentioned my code as being famous. I've always used h and t in one-liners, maybe because of the person who taught me OCaml. H and t can also be found in the official Caml and batteries source code, along with hd/tl, a/l and probably x/xs if it pleases you. I can't agree more about naming variables correctly where it makes sense though.
- waxjar 14y agoI don't know any F# either, but I know that in Ruby the order of files does matter. When you define a class that inherits from a class that lives in another file, you'll have to require that one first.
- pash 14y ago> ... in F#, something to the left of :: can never be anything other than the head of a list, and something to the right of :: can never be anything other than the tail of a list! Yes, but the names you assign in a pattern get used outside the pattern. Presumably there will be an x/head or an xs/tail on the right side of the arrow in your example, and it's there that meaningful names help. That said, I'm quite fond of the x:xs naming convention. It's particularly useful when you're doing something with two or more lists in the same scope, and you pattern-match to produce multiple heads and tails. For instance (Haskell syntax, as I don't know F#): zip :: [a] -> [b] -> [(a, b)] zip (x:xs) (y:ys) = x : y : zip xs ys zip _ _ = [] This also illustrates when you might well disregard the trope of using "meaningful" names. The code above is polymorphic and will work for any types a and b, and for any values of those types, no matter what they represent. Functional programming really encourages this sort of generality. Yes, it's very much the case that, as the article's author wrote, "functional code tends to focus on applying generic transformations to 'things', and not that much on what the 'thing' might be." But, if it helps to clarify what you're doing at the domain level, you can (and should) assign meaning through types and by renaming and adding restrictive type signatures to polymorphic code: data Child = ... data Chore = ... type Assignment = (Child, Chore) assign :: [Child] -> [Chore] -> [Assignment] assign = zip This not only helps readers understand what's going on, it also enlists the type-checker in helping to ensure you've, say, assigned chores only to your kids and not to your pets.
- pash 14y agoWhoops, I changed my example midway through and ended up with something screwy. The first case of the definition of zip above should of course read: zip (x:xs) (y:ys) = (x, y) : zip xs ys
- MichaelGG 14y agoPreferring head::tail over x::xs sorta indicates they have read little functional code. Using x and xs (plural) is pretty common and straightforward. head::tail doesn't help at all. Edit: Also, even in C#, the whole "one class per file" is often unwise. So many times I come across projects that have a bunch of bloody files to define interfaces with a handful of members. And then a bunch of implementations that have 2-line implementations. It's sorta ridiculous. I think it's a holdover from Java's idiotic "tie the filesystem directly into the compilation output" approach.
- NateDad 14y agoActually, if you're browsing the code in Visual Studio, it's a hell of a lot easier to jump around the code if you have one class per file. All the main navigation UI is geared to the file level - the tree in solution explorer is all files. The tabs in the editor are lists of files. ctrl-tab brings up a list of files. You can bring up a new tab group and see two files side by side. So, yes, one thing per file is actually a pretty good idea. Sometimes it's fine to put multiple things into a single file, if there's a lot of small implementations... but for anything even mediumly big, it's good to give it its own file. It helps a lot with navigation. And yes, I know you can do "go to definition" etc, but if you want to go back and forth between code, separate files is a lot easier. It also makes source control a lot easier, because you can see a file changed and know immediately what it will and will not affect. Which is not to say that I don't think java got it wrong, it did, it's just that the problem is not with one class per file, but rather that the file heirarchy matters.
- MichaelGG 14y agoIn C#, VS has a class view that lets you jump around by class name. You can split the window and compare within the same file just fine. And if anything, you're complaining about VS's lack of capabilities. I'm not arguing that you never want a separate file, just that having a forced "one class per file" rule just sprays files all over and means you have to jump around more. Java's hierarchy+class=file is just silly; there's nothing stopping people from enforcing that themselves, if they so choose.
- 14y ago
- tikhonj 14y agoThe x::xs example is bad because it's a widely used idiom, similar to using i as a loop counter. Also, I think that short names are completely justified if the variable is used on a single line. In fact, in those cases, the shorter name is often clearer because it doesn't obscure the structure of the line. So, my rule of thumb: top-level bindings, function arguments for complicated functions and local bindings that are either used in more than one place or away from where they are defined all get descriptive names. (Although I do try to keep even those brief--more than two words is probably too much, and even two is suspicious.) Function arguments to small functions and local variables that are used immediately (usually from pattern matches) get short names--one or two characters, usually. Of course, this really is just a rule of thumb. If deviating from it seems to improve my code, I deviate. But it does give a good idea of the shape of my code.
- gnaritas 14y ago> The x::xs example is bad because it's a widely used idiom, similar to using i as a loop counter. And IMHO bad for the same reason. I'd rather see "index" than "i" and I'd rather see "head::tail" than "x::xs", abbreviations for variable names are terrible; the English language is not running out of words and readability is more important than brevity. Of course, I'm a Smalltalker and we like to actually have readable code.
- eswangren 14y agoA loop counter named "i" is a problem for you? Really? I would hate to see every loop counter named "index". It's completely unnecessary and just adds noise to your code. "i" is not going to confuse anyone, not for one moment. Making that name longer just means you have to type more characters and, honestly, looks ametuerish.
- mich41 14y agoEverybody who has ever seen any C code has #define i index programmed deeply in his brain's CPP. Using i for loop counter never causes confusion and using index makes the for statement 12 characters longer for no reason.
- ajanuary 14y agow.r.t. x::xs vs. head::tail One of the nice conceptual things about pattern matching is the pattern on the left hand side describes which bits of the structure you're interested in, while the stuff on the right hand side describes how to manipulate that data. When you're looking at the rhs it shouldn't care about where data came from, it should just care about what the data is. I've got some arbitrary thing `x` and a list of arbitrary things `xs`. I've got some `person` and a list of `people`. Using `head` and `tail` makes the rhs concerned with the structure of the incoming data. This becomes more of a problem if you have more complicated data structures on the lhs. One of the key elements of readable code is how easy it is to map your conceptual model onto source code onto executable code. Using head::tail wouldn't help new people grok the separation of structure on the lhs and manipulation on the rhs. All that said, it's such a minor point as to probably not really have any impact in this case. Just a bit of silly purist hand-waving.
- ufo 14y agoI really like this analogy. I think a good example of it is the following code for red-back trees: balance :: Color -> RBTree a -> a -> RBTree a -> RBTree a balance B (Fork R (Fork R a x b) y c) z d = Fork R (Fork B a x b) y (Fork B c z d) balance B (Fork R a x (Fork R b y c)) z d = Fork R (Fork B a x b) y (Fork B c z d) balance B a x (Fork R b y (Fork R c z d)) = Fork R (Fork B a x b) y (Fork B c z d) balance B a x (Fork R (Fork R b y c) z d) = Fork R (Fork B a x b) y (Fork B c z d) balance k a x b = Fork k a x b The lhs does the dirty work of identifying the relevant bits in the tree and in the rhs things get rebalanced in a similar form. (Another thing I find amusing in this example is that its one of the few examples of Haskell code where you can't get around the "copypasting" by using `let` or `where`)
- deleted 14y ago[deleted]
- thomasz 14y agoSpeaking about F# and readability: Reading F# (and Ocaml) without an editor sucks: - Nobody bothers to include type or visibility annotations in function prototypes. Signature files are not local and thus are not included in diffs etc. If you think that doesn't matter, go to https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSharp.Core/seq.fs#L261 https://github.com/fsharp/fsharp/blob/master/src/fsharp/FSha... and tell me the type of generateWhileSome. - The order of compilation matters, and is defined in the Build script: > Essentially, I either start from the first line of the first file, and read forward, or the last line of the last file, working my way back." That only works in the IDE. On github, the workflow goes like this: Open the build file, look for the first file. Find that file in the directory structure. Read that file and go back to the build script, rinse, repeat ad nausea.
- kvb 14y agoThere's no reason that this needs to be the case; see Tomas Petricek's tools for producing tooltips for F# code on the web [1][2] [1] http://tomasp.net/blog/fswebsnippets-intro.aspx http://tomasp.net/blog/fswebsnippets-intro.aspx [2] http://tomasp.net/blog/fsharp-literate-programming.aspx http://tomasp.net/blog/fsharp-literate-programming.aspx
- thomasz 14y agoSeems like the only thing left to do is getting patches into cat and diff. Seriously, adding support for inline signatures would solve the problem in a general way. Why not do this? val map : f:('a -> 'b) -> list:'a list -> 'b list let map f list = (*...*)
- lysium 14y agoI'm all with succinctness. I'm wondering how much this has to do with F# per se than with imperative vs. functional programming language. I think the same applies to Java vs. Scala / Clojure. (For example, in German, http://funktionale-programmierung.de/2013/02/26/scala-java-ant.html http://funktionale-programmierung.de/2013/02/26/scala-java-a... -- Google Translate: http://goo.gl/CF1q9 http://goo.gl/CF1q9)
- rcoh 14y ago503....Whoops.
- Jabbles 14y agoCan you really claim that it's a "one-liner" just because you can format it so that it has no newlines? You can do the same thing with C, but I wouldn't let anyone do it in my codebase. Perhaps the F# community feels differently?