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?
4
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.
5
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.
7
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.
2