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?
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.
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.
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