In F#, is there any difference whatsoever between let f = fun a -> a-1
and let f a = a-1
? As far as I can see, the latter is simply syntactic sugar for the former. Is this correct?
I'm looking specifically for semantic differences here, rather than differences in how any particular compiler handles the two cases.
(fun a -> ...)
is simply an anonymous function. F# differs from C# in that functions are first class citizens, so when you bind an anonymous function to f
, it will give f
the type signature val f : int -> int
(because a
is inferred to be int32
), just as if you had bound a normal, named function as in your second example. You can prove that they are semantically identical by running your examples in F# interactive.
Edit: Anonymous functions even support generics which are normally inferred but can be made explicit, like this:
let f<'T> = (fun (x: 'T) -> x)
Edit 2: From the F# 3.1 spec draft (found here):
A value definition is considered a function definition if its immediate right-hand-side is an anonymous function, as in this example:
let f = (fun w -> x + w)
Setting the obvious syntax error aside, what this is saying is that binding a function value (i.e. anonymous function) to an identifier is literally equivalent to a normal function definition. It goes on to say that this equivalence is a "staple of functional programming," so you can be pretty sure this won't change in the future.
For general purposes, F# treats function definitions and function values (i.e. delegate instances, anonymous functions) as equal at design time, but when it needs to lift a function definition to a function value, it will use the delegate type FSharpFunc as the compiled type. This is the case for all higher order functions, like those in the Array, List, Seq modules, and so forth, since there's no actual way to use a CIL method as a function value like you can with delegates. Everything else looks exactly like you'd expect compiled C# or VB.NET to -- it's just delegates for which F# uses FSharpFunc.
Edit 3: I'm not able to add comments yet, but with regards to Tomas' answer, the F# compiler doesn't know how to generalize the expression let h = (); fun a -> a
but it will accept it if you add the type annotations yourself, like with Nikon's id
example.
Edit 4: Here's an awesomely crude picture of how F# compiles functions. Notice how Tomas' example, a sequencing expression as he called it, gets turned into an FSharpFunc, whereas the equivalent function without the ();
becomes an actual CIL method. This is what the F# spec was talking about above. Also, when you use a regular CIL method as a value, with partial application or otherwise, the compiler will make a FSharpFunc to represent it as a closure.
As others already mentioned, the simple answer is that defining a function using let foo x = ...
and using let foo = ...
can give you different results (because of value restriction), but it does not happen in this case, because the compiler is smart enough to know that it can safely treat let foo = fun x -> ..
as let foo x = ...
.
Now, if you want to see more details, try the following definitions:
let f a = a // Function 'a -> 'a
let g = fun a -> a // Function 'a -> 'a
let h = (); fun a -> a // error FS0030: Value restriction
So, the compiler treats a function defined using let foo = fun x -> ...
as an ordinary function as long as there is nothing else before the code that creates & returns the function. If you do anything beofore that (even if it's just ignoring the unit value), then there is a difference.
If you want to learn more, then the section 14.6.7 Generalization in the F# 3.0 specification lists all generalizable values (that is, the expressions for which the above works):
The following expressions are generalizable:
- A function expression
- An object expression that implements an interface
- A delegate expression
- A “let” definition expression in which both the right-hand side of the definition and the body of the expression are generalizable
- A “let rec” definition expression in which the right-hand sides of all the definitions and the body of the expression are generalizable
- A tuple expression, all of whose elements are generalizable
- A record expression, all of whose elements are generalizable, where the record contains no mutable fields
- A union case expression, all of whose arguments are generalizable
- An exception expression, all of whose arguments are generalizable
- An empty array expression
- A constant expression
- An application of a type function that has the GeneralizableValue attribute.
The important thing is the first point - that a function expression (e.g. fun x -> x
) is generalizable; A sequencing expression (e.g. (); fun x -> x
) is not generalizable and so it behaves differently than a plain function.
The examples you provided are semantically the same, but the F# compiler has everything to do with it.
Let's look at a different (generic) function:
// val singleton1 : x:'a -> List<'a>
let singleton1 x = [x]
// val singleton2 : x:'a -> List<'a>
let singleton2 = fun x -> [x]
As you can see, the signatures are the same. But if you think about it, these two shouldn't really be the same: The first one is a true function (compiles to a .NET method), but the second one is simply a value holding a function (delegate or Func in C#). But there are no generic values in the .NET runtime. This only works because the F# compiler is smart enough to make singleton2
a function too.
You can see what I mean here:
let singleton3 = id >> fun x -> [x]
Now here we outsmarted the F# compiler and this won't compile because of Value Restriction (scroll down to the topic), even though it should be semantically the same as singleton2
.
So, to sum up: From a semantic point of view, your definitions are the same, but because of restrictions in the .NET runtime, the F# compiler has to do some extra work to enable this.
One other difference I just remembered is that only functions can be marked inline
:
let inline f a = a-1 // OK
let inline f = fun a -> a-1 // Error
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With