9

I'm trying to learn a bit more about F#'s computation expressions by implementing one of my own. However, I've hit a stumbling block with relation to the Bind method. Here's what I've got so far:

type public op<'a> = Op of ('a list -> 'a list)

let inline (>>) (Op a) (Op b) = Op (a >> b)

module Op =
    let id = Op id
    let bind (b : 'b -> op<'a>) (v : 'b) = b v
    let call (Op f) = f
    let push v = Op (fun t -> v :: t)
    // .. snip  ..

type OpBuilder() =
    member __.Bind (v, b) = Op.bind b v
    member __.Yield (()) = Op.id
    member __.Return (m) = Op.call m

    [<CustomOperation("push")>]
    member __.Push (m : op<'a>, v : 'a) = m >> Op.push v
    // .. snip  ..

let op = new OpBuilder()

Now, my understanding is that all of the following should be roughly equivalent:

// Use only the Op module methods
let f1 = Op.call(Op.bind (fun x -> Op.push x) 1)

// Use op builder with external closure
let f2 = Op.call(let x = 2 in op { push x })

// Use op builder bind explicitly
let f3 = op.Return(op.Bind(3, fun x -> op.Push(op.Yield(), x)))

// Use op builder with let! binding
let f4 = op { let! x = 4 in push x }

The first 3 seem to work fine, however, f4 gives this error:

This expression was expected to have type
  unit
but here has type
  int

I get the same error if I use let instead of let!. Everything I've read suggests that this is correct way to implement Bind, but apparently I'm missing something. Can anyone point out what I'm doing wrong?

Ringil
  • 6,277
  • 2
  • 23
  • 37
p.s.w.g
  • 146,324
  • 30
  • 291
  • 331
  • 1
    What does that workflow represent? I can tell you now that the types of your bind and return are off the mark, at least as far as monadic bind and return goes, but I have no idea what I'm looking at. – scrwtp Jul 15 '16 at 21:53
  • @scrwtp The idea is to create a sort of stack-oriented DSL. e.g. `op { push 2; dup; add } []` → `[4]`. – p.s.w.g Jul 15 '16 at 22:01
  • 2
    What you call `bind` is not a bind, but just function application. To make it a monadic bind, the second parameter should be `op<'b>`, not `'b`. – Fyodor Soikin Jul 15 '16 at 22:05
  • @FyodorSoikin Okay point taken. The ultimate goal was to implement something along the lines of `let swap = op { let! x = pop; let! y = pop; push x; push y }` and I figured starting with constant values (`let! x = 4`) would be easier. `pop` is a kind of `op<'a>` since it modifies the "stack" but I had no clue how to retrieve the popped value as a variable. – p.s.w.g Jul 15 '16 at 22:17
  • @p.s.w.g: What you're really want to arrive at is state monad - it's the piece you're missing to be able to implement `pop` - as its type would correspond to `'a list -> 'a * 'a list`, the lone `'a ` being the popped value. I can give you an answer along these lines, but I wouldn't want to spoil the fun for you. – scrwtp Jul 15 '16 at 22:43
  • @scrwtp right now I have `Op.pop : ('a -> op<'a>) -> op<'a>` (e.g. `pop (fun x -> pop (fun y -> push x >> push y))` but I'm not sure how to translate that to the `OpBuilder`. I'll give your suggestion a try. – p.s.w.g Jul 15 '16 at 23:00
  • @p.s.w.g: I gave it a go and arrived at something that works. It looks and feels like a "broken" state workflow, which is interesting in and of itself. – scrwtp Jul 15 '16 at 23:46
  • See http://stackoverflow.com/questions/14110532/extended-computation-expressions-without-for-in-do perhaps? – kvb Jul 16 '16 at 00:48
  • @kvb I've tried to read the `ILBuilder` source code several times, but it's still hard to get my head around. Do you have any other documentation about the logic and reasoning behind how you designed it? – p.s.w.g Jul 16 '16 at 00:57
  • @p.s.w.g - the ILBuilder code is (hopefully) much more complicated than you need (e.g. it tracks not only the contents of the stack, but also other minutiae of the .NET IL rules, like which instructions can end a method, byref types, etc.). – kvb Jul 16 '16 at 01:50
  • @p.s.w.g - The basic idea is to encode the type of a stack containing values of types `'a0`, `'a1`, ..., `'an` as a generic type `T<'a0 * (a1 * (... * (an * unit)))>` (where there's nothing particularly special about `unit` or the binary tuple constructor `*` - you just need some recongizable way of encoding a type-level empty list and consing a single element to another list). Then, for each operator you care about, you give it a signature that respects the correct semantics (e.g. `pop` takes `T<'a * 'as>` to `T<'as>`, `dup` takes `T<'a * 'as>` to `T<'a * ('a * 'as)>`, etc.). – kvb Jul 16 '16 at 01:54
  • @p.s.w.g - But underneath this layer of typing, you can have your runtime values have a simpler structure, such as Tomas's suggested list of DU values. You'd have something like `type T<'x> = T of Instruction list`, where the type parameter `'x` is a "phantom" type, since it doesn't occur on the right-hand-side of the definition. Then as long as your exposed operations have the right signatures, everything works out fine. Hope that helps - it's a bit involved but hopefully the core concepts aren't too bad. – kvb Jul 16 '16 at 01:58
  • You might be interested that a "stack" monad can be thought of as a [special case](https://gist.github.com/jwosty/5338fce8a6691bbd9f6f#file-statebuilder-fsx-L52) of the more general-purpose state monad. – Jwosty Jul 16 '16 at 04:31

2 Answers2

6

While a proper solution that gives you breathing room to grow involves using a full-fledged state monad as discussed in the comments, you can still get some mileage out of your Op type as defined. You need to define a sort of degenerate builder for it - it's not a monad, it's essentially a "function composition" builder, but it's just expressive enough for do!, so you get a nice looking syntax.

So assuming you have an Op type like this:

type Op<'a> = 'a list -> 'a list

You define your builder like this - with a unit taking place of a "proper" unwrapped value in bind and return - think of it as the part missing from state monad type:

type OpBuilder() =
    member __.Bind (ma, f) = ma >> f ()
    member __.Return (_) = id

let op = new OpBuilder()

Then the operations:

[<AutoOpen>]
module Ops =

    let push (x:'a) : Op<'a> =  fun st -> x::st

    let dup: Op<'a> = fun st ->
        match st with
        | h::t -> h::h::t
        | [] -> []  

    let add: Op<int> = fun st ->
        match st with
        | a::b::t -> (a+b)::t
        | _ -> failwith "not enough operands" 

And finally:

let comp : Op<_> =
    op {
        do! push 2
        do! dup
        do! add
    }

comp [] 
scrwtp
  • 13,437
  • 2
  • 26
  • 30
  • Thanks, I'll have to try this out this a bit more when I'm back in front of my PC. I realized one problem I had was that `Return` was supposed to be `Run` in my builder (that's why I needed `call` in `f2`). – p.s.w.g Jul 16 '16 at 00:33
6

If you are trying to implement something like a stack-based DSL, then computation expressions are not a very good fit. You can perfectly represent this just with a list of operations:

 type Op = 
  | Push of int
  | Dup
  | Add

let sample = 
  [ Push 2
    Dup
    Add ]

And I could not resist writing a simple evaluator too:

let rec eval stack ops = 
  match ops, stack with
  | (Push n)::ops, stack -> eval (n::stack) ops
  | Dup::ops, s::stack -> eval (s::s::stack) ops
  | Add::ops, s1::s2::stack -> eval ((s1+s2)::stack) ops
  | [], stack -> stack
  | _ -> failwith "Wrong arguments"

eval [] sample

Computation expressions are useful if you want to give some special meaning to ordinary language constructs such as variable binding let!, other constructs like for and try or returning a value as captured by yield or return. Although you can implement some of the Haskell monads, those are not all that useful in F# - so it is a bit tricky to find a useful toy example to play with.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • I'm not sure I agree - you can use a computation expression to enforce that you never pop an empty stack, for example, as long as you're willing to tolerate some wonky types. You can do this without using computation expressions, by composing a sequence of methods instead, but you can't achieve it with a list of DU values. – kvb Jul 16 '16 at 00:47
  • This is similar to something I started off with, but came to conclusion that it was too limiting (e.g. it would be hard to define new modules). Also, computation expression builders always seemed like some sort of witchcraft to me, so I wanted to take the opportunity to demystify them a bit. – p.s.w.g Jul 16 '16 at 00:49
  • @p.s.w.g Yep, that makes sense - writing some computation builder is fun :) all I'm trying to say is that it's easier to start with something that fits more into the traditional monad-like structures. – Tomas Petricek Jul 16 '16 at 01:52
  • 2
    @kvb I guess my wording was too strong - you can certainly do that with a builder and it can give you some nice benefits (with wonky types :-)). But I think of the builder as the last step that you may try to add to see if the syntax makes it nicer, once you figured out how exactly composition in your domain works (without computation builders). – Tomas Petricek Jul 16 '16 at 01:53