10

Here is what I have so far:

type Maybe<'a> = option<'a>

let succeed x = Some(x)

let fail = None

let bind rest p =
    match p with
        | None -> fail
        | Some r -> rest r

let rec whileLoop cond body =
    if cond() then
        match body() with
        | Some() ->
            whileLoop cond body
        | None ->
            fail
    else
        succeed()

let forLoop (xs : 'T seq) f =
    using (xs.GetEnumerator()) (fun it ->
            whileLoop
                (fun () -> it.MoveNext())
                (fun () -> it.Current |> f)
        )

whileLoop works fine to support for loops, but I don't see how to get while loops supported. Part of the problem is that the translation of while loops uses delay, which I could not figure out in this case. The obvious implementation below is probably wrong, as it does not delay the computation, but runs it instead!

let delay f = f()

Not having delay also hinders try...with and try...finally.

Joh
  • 2,380
  • 20
  • 31
  • 1
    Have you tried `let delay f = fun () -> f()`? – Daniel Jan 27 '12 at 23:10
  • 1
    Have you taken a look at `Maybe` monad implementation in FSharpx https://github.com/fsharp/fsharpx/blob/master/src/FSharpx.Core/Monad.fs ? – pad Jan 27 '12 at 23:45
  • See http://stackoverflow.com/questions/4577050/what-is-the-role-of-while-loops-in-computation-expressions-in-f for one approach. – kvb Jan 28 '12 at 03:01
  • 1
    In particular, `Delay f = f` and `Run f = f()` should probably work, assuming that you adjust the types of `While`, `Combine`, etc. accordingly. – kvb Jan 28 '12 at 03:02
  • pad: Haven't tried it, but I don't see how it could possibly work. For example, TryWith doesn't do anything with the exception handler. Looks suspicious to me, but maybe it's just me missing something. – Joh Jan 28 '12 at 13:32
  • Daniel, `delay` is supposed to have type `(unit -> M<'T>) -> M<'T>`, which your suggestion does not meet. – Joh Jan 28 '12 at 13:35
  • 2
    @Joh You can define `delay` with a type `(unit -> M<'T>) -> (unit -> M<'T>)` (as suggested by @kvb). If you modify the remaining definitions to match this type, everything will work just fine. See my answer for more complex definition. (The trick is that computation builder can simply use different type for non-delayed and delayed computations). – Tomas Petricek Jan 28 '12 at 16:53

2 Answers2

12

There are actually two different ways of implementing continuation builders in F#. One is to represent delayed computations using the monadic type (if it supports some way of representing delayed computations, like Async<'T> or the unit -> option<'T> type as shown by kkm.

However, you can also use the flexibility of F# computation expressions and use a different type as a return value of Delay. Then you need to modify the Combine operation accordingly and also implement Run member, but it all works out quite nicely:

type OptionBuilder() = 
  member x.Bind(v, f) = Option.bind f v
  member x.Return(v) = Some v
  member x.Zero() = Some ()
  member x.Combine(v, f:unit -> _) = Option.bind f v
  member x.Delay(f : unit -> 'T) = f
  member x.Run(f) = f()
  member x.While(cond, f) =
    if cond() then x.Bind(f(), fun _ -> x.While(cond, f)) 
    else x.Zero()

let maybe = OptionBuilder()

The trick is that F# compiler uses Delay when you have a computation that needs to be delayed - that is: 1) to wrap the whole computation, 2) when you sequentially compose computations, e.g. using if inside the computation and 3) to delay bodies of while or for.

In the above definition, the Delay member returns unit -> M<'a> instead of M<'a>, but that's perfectly fine because Combine and While take unit -> M<'a> as their second argument. Moreover, by adding Run that evaluates the function, the result of maybe { .. } block (a delayed function) is evaluated, because the whole block is passed to Run:

// As usual, the type of 'res' is 'Option<int>'
let res = maybe { 
    // The whole body is passed to `Delay` and then to `Run`
    let! a = Some 3
    let b = ref 0
    while !b < 10 do 
      let! n = Some () // This body will be delayed & passed to While
      incr b
    if a = 3 then printfn "got 3"
    else printfn "got something else"
    // Code following `if` is delayed and passed to Combine
    return a }

This is a way to define computation builder for non-delayed types that is most likely more efficient than wrapping type inside a function (as in kkm's solution) and it does not require defining a special delayed version of the type.

Note that this problem does not happen in e.g. Haskell, because that is a lazy language, so it does not need to delay computations explicitly. I think that the F# translation is quite elegant as it allows dealing with both types that are delayed (using Delay that returns M<'a>) and types that represent just an immediate result (using Delay that returns a function & Run).

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • 1
    Great answer. Is it just Haskell's laziness that comes into play here? A while loop doesn't even make sense in Haskell because it's dependent on side effects, right? – kvb Jan 28 '12 at 23:16
  • Also, I'm a bit surprised that your `Zero` and `Combine` aren't more like the typical `MonadPlus` instance for `Maybe` in Haskell, although I suppose it's all a question of what semantics you expect. – kvb Jan 28 '12 at 23:20
  • @kvb Good point about `while` loop. I guess that a `while` loop might make sense in the context of monads, because the monad may provide side-effects that would make such construct useful in Haskell. The condition function would have to be monadic too though. – Tomas Petricek Jan 28 '12 at 23:23
  • @kvb Regarding `Combine` and `Zero` - I think these appear in two different context. You can use them similarly to `MonadPlus` if your monad can return multiple results (which would be probably written using `yield`). Then `Combine` combines the values and `Zero` returns no values. However, they can be also used (like here) for sequential composition of side-effecting computations. Here, `Zero` returns computation that does not return (after performing side-effects) and `Combine` combines computation that has side-effects & doesn't return (first arg.) with computation that returns a value. – Tomas Petricek Jan 28 '12 at 23:26
  • 1
    Thanks Tomas. I assumed I had to conform to the signatures found in the docs http://msdn.microsoft.com/en-us/library/dd233182.aspx, but it seems those are meant as a guide, not strict rules. What really matters is that all methods can be combined in the way describe in the second table, right? – Joh Jan 29 '12 at 07:21
5

According to monadic identities, your delay should always be equivalent to

let delay f = bind (return ()) f

Since

val bind : M<'T> -> ('T -> M<'R>) -> M<'R>
val return : 'T -> M<'T>

the delay has the signature of

val delay : (unit -> M<'R>) -> M<'R>

'T being type-bound to unit. Note that your bind function has its arguments reversed from the customary order bind p rest. This is technically same but does complicate reading code.

Since you are defining the monadic type as type Maybe<'a> = option<'a>, there is no delaying a computation, as the type does not wrap any computation at all, only a value. So you definition of delay as let delay f = f() is theoretically correct. But it is not adequate for a while loop: the "body" of the loop will be computed before its "test condition," really before the bind is bound. To avoid this, you redefine your monad with an extra layer of delay: instead of wrapping a value, you wrap a computation that takes a unit and computes the value.

type Maybe<'a> = unit -> option<'a>

let return x = fun () -> Some(x)

let fail = fun() -> None

let bind p rest =
    match p() with
    | None -> fail
    | Some r -> rest r

Note that the wrapped computation is not run until inside the bind function, i. e. not run until after the arguments to bind are bound themselves.

With the above expression, delay is correctly simplified to

let delay f = fun () -> f() 
  • Thanks for the answer, it confirms what I was suspecting: I need to build upon continuations. I'm still surprised by the differences between `for` and `while`. It looks a bit inconsistent to me. – Joh Jan 28 '12 at 13:38
  • @kkm Just out of curiosity, do you have any reference for the statement that "delay should always be equivalent to ..."? There is a perfectly fine alternative definition when implementing F# computatione expressions, but I'd be quite interested if similar problem already appeared somewhere else (I guess Haskell doesn't have that problem and theoretical "monad" also doesn't have any delay...) – Tomas Petricek Jan 28 '12 at 16:49
  • @TomasPetricek: Right, I am mixing the theoretical monad and F# (eager) implementation. The extra identity for delay was in Syme et al Expert F# (Ch. 9 in the old (not 2.0) book I have here at home). I never completely explored what would happen if that one is violated, so kinda sticking to it to stay on the safe side. And I like your approach better than mine, really! – kkm inactive - support strike Jan 28 '12 at 18:01
  • @kkm Thanks for the reference! I suppose that my approach is more "dangerous" in the sense that defining `Delay` using `Bind` (and using `M<'T>` type everywhere) is a safe option. Although I think that using the alternative works too. I'd be interested to see if Expert F# 2.0 says the same thing! – Tomas Petricek Jan 28 '12 at 20:29
  • @kkm I was especially curious to know if something like `Delay` exists in other languages... – Tomas Petricek Jan 28 '12 at 20:31
  • @TomasPetricek: I am sure the 2.0 book does, I only DNK if it has same chapter numbering. Still I am sure that your suggestion is correct from the practical standpoint, and more efficient, avoiding building and running continuations. – kkm inactive - support strike Jan 28 '12 at 22:59
  • @TomasPetricek: I do not know. I think that Scala does not, OCaml has no builders, although has a `perform` extension, and Haskell does not need delay. Lisp, Erlang... Nah. I do not know many functional languages to answer systematically to any degree. – kkm inactive - support strike Jan 28 '12 at 23:09