Why does this point-free F# function behave differently from the non-point-free version?

272 views Asked by At

Consider the following F#:-

type TestClass() =
    let getValFromMap m k = Map.find k m

    let addToMap map k i = map |> Map.add k i

    let mutable someMap : Map<string,int> = Map.empty

    let getValFromMapPartial key = getValFromMap someMap key
    let getValFromMapPartialAndTacit = getValFromMap someMap

    member this.AddThenGet() =
        someMap <- addToMap someMap "A" 10

        let value = getValFromMapPartial "A"
        printfn "Value from partial = %i" value   // prints out

        let value = getValFromMapPartialAndTacit "A"  // throws
        printfn "Value from partial and tacit = %i" value

[<EntryPoint>]
let main argv = 
    let test = TestClass()
    test.AddThenGet()
    0

Functions getValFromMapPartial and getValFromMapPartialAndTacit are, to my mind, identical. F# says they have the exact same type: (string -> int). And yet they behave very differently, and they are compiled very differently. Decompiling using dotPeek, I see that getValFromMapPartial is a method, whereas getValFromMapPartialAndTacit is a field that is initialized in the ctor.

F# does not complain about getValFromMapPartialAndTacit, even on the highest warning level (both in VS 2012 and 2013). And yet calling this function in my sample above fails, presumably because it has wrapped the initial, empty version of the someMap, despite its mutability.

Why is there a difference between these two functions? Should there be a warning from F# that the tacit / point-free version might fail?

2

There are 2 answers

3
Vandroiy On BEST ANSWER

The F# compiler distinguishes between let-bindings of functions, which have parameters, and values, which do not have parameters.

  • Value definition: A binding like let a = ... is a value definition. Its body is evaluated eagerly, "where it is", before the evaluation of anything further down the code.

  • Function definition: A binding like let f x = ... is a syntactic function definition, the contents of which are evaluated when the function is called.

Since someMap refers to a mutable variable, using this variable inside a function definition means reading from the variable when the function is called. However, the usage in getValFromMapPartialAndTacit reads the value at the moment of declaration.

This behavior does not stop a value from being a function. You could just as well write let f = fun x -> ... to declare a function, and ... would again be part of a function definition. However, if you were to add definitions in between the = and fun, they would be evaluated at the point of the definition of f, not when it is called.


In the question's comments, the same problem occurs with someMap being a mutable reference cell. This is the same problem. The function, as rewritten by Andrew for a mutable reference cell:

let getValFromMapPartialAndTacit = getValFromMap !someMap

Here, the dereference operator (!) is applied when the value is bound, not when the function is called. it is equivalent to:

let mapRightNow = !someMap
let getValFromMapPartialAndTacit = getValFromMap mapRightNow
9
Marc Sigrist On

getValFromMapPartial is a true syntactic function. Its signature is val getValFromMapPartial : key:string -> int. Whenever it is called, it uses the current value of someMap. That's why it works in your example; it accesses the version of someMap who has an entry.

On the other hand, getValFromMapPartialAndTacit is a lambda-computing function. Its signature is val getValFromMapPartialAndTacit : (string -> int) (notice the parentheses). The lambda has a compiler-generated closure, which contains the version of someMap at the time the lambda was computed. That's why it does not work in your example; it always acesses the same, original version of someMap who has no entry.