29

To make this question answerable, let's assume that the cost of ambiguity in the mind of a programmer is much more expensive then a few extra keystrokes.

Given that, why would I allow my teammates to get away with not annotating their function parameters? Take the following code as an example of what could be a far more complex piece of code:

let foo x y = x + y

Now, a quick examination of the tooltip will show you that F# has determined you meant for x and y to be ints. If that's what you intended, then all is well. But I don't know if that's what you intended. What if you had created this code to concatenate two strings together? Or what if I think you probably meant to add doubles? Or what if I just don't want to have to hover the mouse over every single function parameter to determine its type?

Now take this as an example:

let foo x y = "result: " + x + y

F# now assumes you've probably intended to concatenate strings, so x and y are defined as strings. However, as the poor schmuck who's maintaining your code, I might look at this and wonder if perhaps you had intended to add x and y (ints) together and then append the result to a string for UI purposes.

Certainly for such simple examples one could let it go, but why not enforce a policy of explicit type annotation?

let foo (x:string) (y:string) = "result: " + x + y

What harm is there in being unambiguous? Sure, a programmer could choose the wrong types for what they are trying to do, but at least I know they intended it, that it wasn't just an oversight.

This is a serious question... I am still very new to F# and am blazing the trail for my company. The standards I adopt will likely be the basis for all future F# coding, embedded in the endless copy-pasting that I am sure will permeate the culture for years to come.

So... is there something special about F#'s type inference that makes it a valuable feature to hold onto, annotating only when necessary? Or do expert F#-ers make a habit of annotating their parameters for non-trivial applications?

JDB
  • 628
  • 8
  • 15
  • The annotated version is less general. It's for the same reason you preferably use IEnumerable<> in signatures and not SortedSet<>. – Patrick Dec 16 '13 at 22:54
  • 2
    @Patrick - No, it's not, because F# is inferring the string type. I've not changed the signature of the function, only made it explicit. – JDB Dec 16 '13 at 22:55
  • 1
    @Patrick - And even if it were true, can you explain why that's a bad thing? Perhaps the signature *should* be less general, if that's what the programmer had intended. Perhaps the more general signature is actually causing problems, but I'm not sure how specific the programmer intended to be without a great amount of research. – JDB Dec 16 '13 at 23:04
  • 1
    I think it's fair to say that, in functional languages, you prefer generality, and annotate in the more specific case; e.g. when you want better performance, and can get it using type hinting. Using annotated functions everywhere would require you to write overloads for each specific type, which may not be warranted in the general case. – Robert Harvey Dec 16 '13 at 23:08

3 Answers3

30

I don’t use F#, but in Haskell it is considered good form to annotate (at least) top-level definitions, and sometimes local definitions, even though the language has pervasive type inference. This is for a few reasons:

  • Reading
    When you want to know how to use a function, it’s incredibly useful to have the type signature available. You can simply read it, rather than trying to infer it yourself or relying on tools to do it for you.

  • Refactoring
    When you want to alter a function, having an explicit signature gives you some assurance that your transformations preserve the intent of the original code. In a type-inferred language, you may find that highly polymorphic code will typecheck but not do what you intended. The type signature is a “barrier” that concretises type information at an interface.

  • Performance
    In Haskell, the inferred type of a function may be overloaded (by way of typeclasses), which may imply a runtime dispatch. For numeric types, the default type is an arbitrary-precision integer. If you don’t need the full generality of these features, then you can improve performance by specialising the function to the specific type you need.

For local definitions, let-bound variables, and formal parameters to lambdas, I find that type signatures usually cost more in code than the value they would add. So in code review, I would suggest you insist on signatures for top-level definitions and merely ask for judicious annotations elsewhere.

Jon Purdy
  • 20,437
  • 7
  • 63
  • 95
  • 3
    This sounds like a very reasonable and well-balanced response. – JDB Dec 17 '13 at 00:57
  • 1
    I agree that having the definition with the types explicitly defined can help when refactoring, but I find that unit testing can also solve this problem and because I believe that unit test should be used with functional programming to ensure a function works as designed before using the function with another function, this allows me to leave the types out of the definition and have the confidence that if I make a breaking change it will be caught. [Example](https://github.com/EricGT/ArithmeticExpressionEvaluator/blob/master/AEE.Core.Tests/ParserCombinator.fs#L363) – Guy Coder Dec 17 '13 at 11:27
  • 4
    In Haskell it's considered correct to annotate your functions, but I believe idiomatic F# and OCaml tend to omit annotations unless necessary to remove ambiguity. That isn't to say it's harmful (though the syntax for annotating in F# is uglier than in Haskell). – KChaloux Dec 17 '13 at 14:29
  • 4
    @KChaloux In OCaml, there are already type annotations in the interface (`.mli`) file (if you write one, which you are strongly encouraged to. Type annotations tend to be omitted from definitions because they'd be redundant with the interface. – Gilles 'SO- stop being evil' Dec 27 '13 at 22:07
  • 1
    @Gilles @KChaloux indeed, same in F# signature (`.fsi`) files, with one caveat: F# doesn't use signature files for type inference, so if something in the implementation is ambiguous, you'll _still_ have to annotate it again. https://books.google.ca/books?id=XKhPCwAAQBAJ&lpg=PA178&ots=GHWjkeGMaS&dq=f%23%20signature%20types%20checked&pg=PA178#v=onepage&q=f%23%20signature%20types%20checked&f=false – Yawar Amin Oct 29 '16 at 21:19
1

Jon gave a reasonable answer which I won't repeat here. I will however show you an option that might satisfy your needs and in the process you will see a different type of answer other than a yes/no.

Lately I have been working with parsing using parser combinators. If you know parsing then you know you typically use a lexer in the first phase and a parser in the second phase. The lexer converts text to tokens and the parser converts tokens into an AST. Now with F# being a functional language and combinators being designed to be combined, parser combinators are designed to make use of the same functions in both the lexer and the parser yet if you set the type for the parser combinator functions you can only use them to lex or to parse and not both.

For example:

/// Parser that requires a specific item.

// a (tok : 'a) : ('a list -> 'a * 'a list)                     // generic
// a (tok : string) : (string list -> string * string list)     // string
// a (tok : token)  : (token list  -> token  * token list)      // token

or

/// Parses iterated left-associated binary operator.


// leftbin (prs : 'a -> 'b * 'c) (sep : 'c -> 'd * 'a) (cons : 'd -> 'b -> 'b -> 'b) (err : string) : ('a -> 'b * 'c)                                                                                    // generic
// leftbin (prs : string list -> string * string list) (sep : string list -> string * string list) (cons : string -> string -> string -> string) (err : string) : (string list -> string * string list)  // string
// leftbin (prs : token list  -> token  * token list)  (sep : token list  -> token  * token list)  (cons : token  -> token  -> token  -> token)  (err : string) : (token list  -> token  * token list)   // token

Since the code is copyright I won't include it here but it is available at Github. Do not copy it here.

For the functions to work they must be left with the generic parameters, but I include comments that show the inferred types depending upon the functions use. This makes it easy to understand the function for maintenance while leaving the function generic for use.

Guy Coder
  • 939
  • 9
  • 13
  • 1
    Why wouldn't you write the generic version into the function? Wouldn't that work? – svick Dec 17 '13 at 01:55
  • @svick I don't understand your question. Do you mean that I left the types out of the function definition, but could have added the generic types to the function definitions since the generic types would not change the meaning of the function? If so the reason is more of a personal preference. When I first started with F# I did add them. When I started working with more advanced users of F# they preferred them to be left as comments because it was easier to modify the code without the signatures there. ... – Guy Coder Dec 17 '13 at 11:14
  • In the long run the way I show them as comments worked for everyone once the code was working. When I start writing a function I put in the types. Once I have the function working, I move the type signature to a comment and remove as many of the types as possible. Some times with F# you need to leave in the type to help the inferencing. For a while I was experimenting with creating the comment with the let and = so that you could uncomment the line for testing, then comment it again when done, but they looked silly and adding a let and = is not that hard. – Guy Coder Dec 17 '13 at 11:18
  • If these comments answer what you are asking let me know so I can move them into the answer. – Guy Coder Dec 17 '13 at 11:19
  • Another reason I don't add just the generic signature to the function definition is that it makes the code look messier. Also adding the generic types typically does not really help in understanding the function as you have to keep the specific type definition in your head to really understand the function in context. When working with many functions its just not easy and is why I use the comments as I did. It's a personal preference. The more I work with F# the more I prefer to leave out the types in the definition. It's an acquired taste but once you acquire it you really to prefer it. – Guy Coder Dec 17 '13 at 11:35
  • 2
    So when you modify the code, you don't modify the comments, leading to outdated documentation? That doesn't sound like an advantage to me. And I don't really see how is messy comment any better than messy code. To me, it seems like this approach combines the disadvantages of the two options. – svick Dec 17 '13 at 14:53
  • Of course I modify the comments if I modify the code. It's hard to put a good answer in comments. Also I never said the code was messy, I said "it makes the code look messier". – Guy Coder Dec 17 '13 at 15:31
-8

The number of bugs is directly proportional to the number of characters in a program!

This is usually stated as the number of bugs being proportional to lines of code. But having less lines with the same amount of code gives the same amount of bugs.

So while it's nice to be explicit, any extra parameters you type can lead to bugs.

James Anderson
  • 18,049
  • 1
  • 42
  • 72
  • 5
    “The number of bugs is directly proportional to the number of characters in a program!” So we should all switch to [APL](https://en.wikipedia.org/wiki/APL_%28programming_language%29)? Being too terse is a problem, just like being too verbose. – svick Dec 17 '13 at 01:57
  • 5
    "*To make this question answerable, let's assume that the cost of ambiguity in the mind of a programmer is much more expensive then a few extra keystrokes.*" – JDB Dec 17 '13 at 02:21