Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should I see a difference when leaving out the unit parameter in this bind() argument?

Tags:

.net

f#

In this F# code from the computation expressions section of the F Sharp Programming Wikibook:

let addThreeNumbers() =
    let bind(input, rest) =
        match System.Int32.TryParse(input()) with
        | (true, n) when n >= 0 && n <= 100 -> rest(n)
        | _ -> None

    let createMsg msg = fun () -> printf "%s" msg; System.Console.ReadLine()

    bind(createMsg "#1: ", fun x ->
        bind(createMsg "#2: ", fun y ->
            bind(createMsg "#3: ", fun z -> Some(x + y + z) ) ) )

When I convert input() to input and create Msg msg from fun () -> printf "%s" msg; System.Console.ReadLine() to printf "%s" msg; System.Console.ReadLine():

let addThreeNumbers() =
    let bind(input, rest) =
        match System.Int32.TryParse(input) with
        | (true, n) when n >= 0 && n <= 100 -> rest(n)
        | _ -> None

    let createMsg msg = printf "%s" msg; System.Console.ReadLine()

    bind(createMsg "#1: ", fun x ->
        bind(createMsg "#2: ", fun y ->
            bind(createMsg "#3: ", fun z -> Some(x + y + z) ) ) )

the program seems to behave exactly the same when I run it on dotnetfiddle.net. Is this just an edge case where the unit parameter isn't actually needed to delay the computations since they're reliant on user input from Console.ReadLine(), or is the modified version incorrect or does it otherwise behave differently in a way I haven't noticed?

like image 256
Ashok Bhaskar Avatar asked Apr 05 '18 19:04

Ashok Bhaskar


1 Answers

In practice

You are correct. In this case, the fact that the input computation is "deferred" is completely superfluous, because it gets unconditionally "un-deferred" on the first line of bind, so there is no possible scenario in which the deferred computation would be run later or not at all.

One subtle difference (which does not matter in practice) is this: in the original code, Console.ReadLine is called from within bind, but in your modified code, Console.ReadLine is called before bind, and its result is then passed into bind.

If bind was somehow more complicated (say, if it had a try .. with block around input() or something like that), then this difference would have mattered. As it is, however, deferment adds nothing.


But in theory

Another way you could see a difference is if you prepare the reading actions "in advance" instead of creating them on the spot:

let msg1 = createMsg "#1: "
let msg2 = createMsg "#2: "
let msg3 = createMsg "#3: "
bind(msg1, fun x ->
    bind(msg2, fun y ->
        bind(msg3, fun z -> Some(x + y + z) ) ) )

With this code, the original bind would work fine, but your modified bind would cause all three inputs to happen every time instead of stopping on first invalid input.

While this looks random on the surface, it actually illustrates an important consideration in program design: difference between evaluation and execution. In plain terms, "evaluation" can be understood as "preparing for work", while "execution" can be understood as "actually doing the work". In my snippet above, the line let msg1 = represents evaluation of the input-reading action, while the call to bind(msg1, ...) represents execution of that action.

Understanding this difference well can lead to better program design. For example, when evaluation is guaranteed to be separate from execution, it can be optimized, or cached, or instrumented, etc., without changing the meaning of the program. In such languages as Haskell, where the very design of the language guarantees separation of evaluation and execution, the compiler gets unprecedented freedom for optimization, resulting in much faster binary code.

While I haven't read the book you're referring to, I would guess that the purpose of this example is in demonstrating this difference, so that, while there is no practical point in deferring computation, there might be an educational one.

like image 51
Fyodor Soikin Avatar answered Nov 15 '22 07:11

Fyodor Soikin