Skip to content

Instantly share code, notes, and snippets.

@nojaf
Created August 9, 2017 13:57
Show Gist options
  • Save nojaf/aa8b191e2f21c291fa58e80076f3c92f to your computer and use it in GitHub Desktop.
Save nojaf/aa8b191e2f21c291fa58e80076f3c92f to your computer and use it in GitHub Desktop.
Computation Expressions question
// Learn more about F# at http://fsharp.org
// See the 'F# Tutorial' project for more help.
open System
let toInt a =
match (Int32.TryParse(a)) with
| (true, x) -> Some x
| (false, _) -> None
type MaybeBuilder() =
member this.Bind(x, f) =
printfn "x: %A, f: %A" x f
match x with
| None -> None
| Some a -> f a
member this.Return(x) =
Some x
let maybe = new MaybeBuilder()
[<EntryPoint>]
let main argv =
printfn "%A" argv
let parsed =
maybe {
let! head = Array.tryHead argv
let! v = head |> toInt
return v
}
printfn "parsed is %A" parsed
0 // return an integer exit code
@nojaf
Copy link
Author

nojaf commented Aug 9, 2017

If the code above is ran without a argument the following gets printed:

PM> .\ComputedExpressionDemo.exe
[||]
x: <null>, f: <fun:parsed@30>
parsed is <null>

When given an valid argument:

[|"19"|]
x: Some "19", f: <fun:parsed@30>
x: Some 19, f: <fun:parsed@31-1>
parsed is Some 19

I'm a bit confused why the first attempt only has one print of line 13?
And if head is None the code in the next computation will not be executed but how exactly is None returned?
Is it because it is the last value within the maybe block?

@jindraivanek
Copy link

It's because second Bind is inside f of first Bind.

@mavnn
Copy link

mavnn commented Aug 9, 2017

So on the first run your CE expands to something like:

maybe.Bind(None, fun o -> maybe.Bind(...))

With the code as written, the bind will never run the continuation passed as the second argument as it will just return None, making the overall return value of the CE None

@jovaneyck
Copy link

jovaneyck commented Aug 9, 2017

As @mavnn mentioned, things might make more sense if you de-sugar the expression.
That f argument that gets passed into Bind is called a continuation, which represents "the next steps after this statement" (I first encountered this pattern when working with async code, it's similar to that). F# for fun and profit has a nice explanation on continuations and Bind which might help clarify things.

In short: every let! is translated to a call to Bind, with the first argument being the option value (x) and the second argument a function that represents all the next steps in the expression (f). Not all those Bind calls get evaluated, so not all those continuations get evaluated either. As soon as you hit a None case in a call to Bind, that Bind call will just return None and not even try to evaluate the continuation, i.e. no additional printfs will be evaluated.

@jovaneyck
Copy link

PS: it's not you. This is as tricky as functional programming in F# gets and the official documentation is terrible :)

@nojaf
Copy link
Author

nojaf commented Aug 9, 2017

Thanks for the quick responses!
So if I change the maybe block to use the maybe instance I get:

let parsedTwo = 
    maybe.Bind
        (tryHead argv, 
         fun head -> 
             maybe.Bind(toInt head, fun result -> 
                 maybe.Return(result)
             )
         )

if tryHead argv is None then the fun head -> ... never get's executed right?

See https://dotnetfiddle.net/2re2RR

@jovaneyck
Copy link

@nojaf correct!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment