Functions in .NET and F# in particular have some oddities that I've run into and can be a bit annoying at times. At least partially for my own benefit, I'm documenting my finds here while I remember them.
So in F#, if you want to define a function that adds two values together, you can write it very simply:
let add x y = x + y
This definition for
add almost doesn't seem worth writing, but it can be
helpful if, say, you want to perform some logging whenever it's used. This
function works great when you go to call it too:
let addNumbers = add 5 10 let addStrings = add "a" "b"
When I said it works great, what I actually meant is that it works great
sometimes. If you define
add as I did and then try to use it like this, you
get two errors from the compiler on the
addStrings line, telling you that
"b" are supposed to be integers, not strings. So how do we fix this?
Well, you could try giving it a type signature:
let add (x : 'a) (y : 'a) : 'a = x + y
Except this doesn't work. Rather than fixing things, it adds two warnings now,
telling you that the
addNumbers line is restricting the type
'a in the new
signature to be integers. So taking the time to tell the compiler that this
function is generic doesn't work well like this, as the compiler decides that
you're using it for integers anyway, and it ignores your type signature.
When I started working with F#, I had actually played around with Haskell a bit already, so my initial thought on how to fix this was to give the type signature like this instead:
let add : 'a -> 'b -> 'c = fun x y -> x + y
If you look through some of my public F# code and search for
actually find that I've used this style frequently. One of the base points
everyone makes about functions in a functional programming language is that
functions are always values, so writing them as a lambda like this shouldn't
make any difference.
Moving on, here's how you actually fix
let inline add x y = x + y
The definitions for
addStrings work correctly now! Yay! Now
if you, like me, had attempted to use full type signatures like
'a -> 'b -> 'c
because you were used to doing that in Haskell, you'll find that there's no way
to fix your code. Neither of these works:
let inline add : 'a -> 'b -> 'c = fun x y -> x + y let add : 'a -> 'b -> 'c = inline fun x y -> x + y
The reason here is that in F#,
fun is used to define a lambda, and while
lambdas are functions, they aren't. Makes sense, doesn't it? Equally confusing,
both of these definitions compile down to the same CIL code:
let normalFunc x y = x + y let lambdaFunc : 'a -> 'b -> 'c = fun x y -> x + y
So the compiled versions are identical, but one is a function and the other isn't. Clear as mud.
At some point since writing my F# and WPF article, I found
myself trying to port a View Model to F#, but couldn't get the messaging tools
in MvvmLight to work for me. After reviewing the 5.3.0 release notes recently, I
found out why I had such difficulty. In the messaging tools, you pass an
Action<T> to a
Register function, where
T is the type of message you want
to process. In F# this is easy to do:
GalaSoft.MvvmLight.Messaging.Messenger.Default.Register ( self, fun x -> printfn "Received a message: %s" x )
This should work, and the F# compiler happily converts our lambda into an
Action<String> object, and passes it along to the
Unfortunately, if you wait around for this lambda to be called, you'll likely
never see it happen. The trick here is that the
Messenger class keeps a
reference to your function with a
WeakReference. When the F# compiler
helpfully converted your lambda into an
Action<String>, it did so by stashing
your lambda somewhere, and then replacing it with
System.Action<String>(YourLambdaHere). Since this new
Action<String> is only
referenced here and the
Messenger class only keeps it in a
the garbage collector decides that it's free to clean it up.
Writing this inside of a class you're using, you could try fixing this like so:
type YourType() as self = let messageHandler = new System.Action<String>(fun x -> printfn "Received a message: %s" x) do GalaSoft.MvvmLight.Messaging.Messenger.Default.Register(self, messageHandler)
Writing the code like this better matches how you're likely to have written the
code in VB or C#, and now
messageHandler can use
self to access members in
the current instance of
YourType; you could even replace the lambda here with
the name of an existing
YourType and it would compile and run.
Except this still doesn't work. Defining
messageHandler here in a type creates
it in the type's constructor; as with the last attempt, as soon as the
constructor ends, the garbage collector decides it can get rid of your handler.
The only way to make sure that your handler stays around for as long as the
YourType does is to mark it with the dreaded
mutable keyword. Do
that, and now this works as expected:
type YourType() as self = let mutable messageHandler = new System.Action<String>(fun x -> printfn "Received a message: %s" x) do GalaSoft.MvvmLight.Messaging.Messenger.Default.Register(self, messageHandler)
This code still creates the
Action<String> during the constructor, but
messageHandler is now an
internal field on the type, and sticks around as it
would if you had written this in VB or C#. Unfortunately, you can't avoid this
by just moving to a member; adding
member x.Handler = new System.Action...
adds a read-only property to your type that returns a new
time it's accessed. This puts you right back in the same boat again, and you
can't (that I know of) add the
mutable keyword to a member.
At this point, all you can really do is try to streamline the effort of creating
messageHandler value. A helper function like this one works:
let BuildAction<'t> f = new System.Action<'t>(f) ... let mutable messageHandler = BuildAction (fun x -> printfn "Received a message: %s" x)
This still works because, again, the
mutable keyword forces the
messageHandler to be stored as a field. This also makes it easy enough to
point to an existing
member definition, so it's about as far as I went.
In case you glossed over most of that, to summarize: a function in F# is always a function, except when it isn't because it's a value, but you can still treat that value as a function except when you can't. Make sense?