3

I'm trying to work out how to use a computation builder to represent a deferred, nested set of steps.

I've got the following so far:

type Entry =
    | Leaf of string * (unit -> unit)
    | Node of string * Entry list * (unit -> unit)

type StepBuilder(desc:string) =
    member this.Zero() = Leaf(desc,id)
    member this.Bind(v:string, f:unit->string) =
        Node(f(), [Leaf(v,id)], id)
    member this.Bind(v:Entry, f:unit->Entry) =
        match f() with
        | Node(label,children,a) -> Node(label, v :: children, a)
        | Leaf(label,a) -> Node(label, [v], a)


let step desc = StepBuilder(desc)

let a = step "a" {
    do! step "b" {
        do! step "c" {
            do! step "c.1" {
                // todo: this still evals as it goes; need to find a way to defer
                // the inner contents...
                printfn "TEST"
            }
        }
    }
    do! step "d" {
        printfn "d"
    }
}

This produces the desired structure:

A(B(C(c.1)), D)

My issue is that in building the structure up, the printfn calls are made.

Ideally what I want is to be able to retrieve the tree structure, but be able to call some returned function/s that will then execute the inner blocks.

I realise this means that if you have two nested steps with some "normal" code between them, it would need to be able to read the step declarations, and then invoke over it (if that makes sense?).

I know that Delay and Run are things that are used in deferred execution for computation expressions, but I'm not sure if they help me here, as they unfortunately evaluate for everything.

I'm most likely missing something glaringly obvious and very "functional-y" but I just can't seem to make it do what I want.


Clarification

I'm using id for demonstration, they're part of the puzzle, and I imagine how I might surface the "invokable" parts of my expression that I want.

Clint
  • 6,133
  • 2
  • 27
  • 48
  • 1
    Here's a clarifying question for you: consider the computation `step "a" { if Console.ReadLine() = "x" then do! step "x" { () } else do! step "y" { () } }`. When such computation is evaluated "for structure only", should it yield `A(X)` or `A(Y)`? – Fyodor Soikin Aug 15 '17 at 03:57
  • @FyodorSoikin see, it's these sorts of logical insights that show **why** the damned thing seems to be impossible at first glance, it's because the overarching structure and logic makes what I want to do seem silly. I suppose really I'm just after a data structure with a bit of functionality tacked on, but I don't *really* want bind as part of the declaration. I'll keep looking, but I probably just want a declaration step, and then some sort of translation step to collapse it into the final form I'm after. – Clint Aug 15 '17 at 10:12

2 Answers2

6

As mentioned in the other answer, free monads provide a useful theoretical framework for thinking about this kind of problems - however, I think you do not necessarily need them to get an answer to the specific question you are asking here.

First, I had to add Return to your computation builder to make your code compile. As you never return anything, I just added an overload taking unit which is equivalent to Zero:

member this.Return( () ) = this.Zero()

Now, to answer your question - I think you need to modify your discriminated union to allow delaying of computations that produce Entry - you do have functions unit -> unit in the domain model, but that's not quite enough to delay a computation that will produce a new entry. So, I think you need to extend the type:

type Entry =
  | Leaf of string * (unit -> unit)
  | Node of string * Entry list * (unit -> unit)
  | Delayed of (unit -> Entry)

When you are evaluating Entry, you will now need to handle the Delayed case - which contains a function that might perform side-effect such as printing "TEST".

Now you can add Delay to your computation builder and also implement the missing case for Delayed in Bind like this:

member this.Delay(f) = Delayed(f)
member this.Bind(v:Entry, f:unit->Entry) = Delayed(fun () ->
    let rec loop = function
      | Delayed f -> loop (f())
      | Node(label,children,a) -> Node(label, v :: children, a)
      | Leaf(label,a) -> Node(label, [v], a)
    loop (f()) )

Essentially, Bind will create a new delayed computation that, when called, evaluates the entry v until it finds a node or a leaf (collapsing all other delayed nodes) and then does the same thing as what your code did before.

I think this answers your question - but I'd be a bit careful here. I think computation expressions are useful as a syntactic sugar, but they are very harmful if you think about them more than you think about the domain of the problem that you are actually solving - in the question, you did not say much about your actual problem. If you did, the answer might be very different.

Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • 1
    Thanks for this, chances are incredibly high I'm seeing nails everywhere and CE's are my hammer! Good learning opportunity though! – Clint Aug 16 '17 at 11:56
4

You wrote:

Ideally what I want is to be able to retrieve the tree structure, but be able to call some returned function/s that will then execute the inner blocks.

This is an almost perfect description of the "free monad", which is basically the functional-programming equivalent of the OOP "interpreter pattern". The basic idea behind the free monad is that you convert imperative-style code into a two-step process. The first step builds up an AST, and the second step executes the AST. That way you can do things in between step 1 and step 2, like analyze the tree structure without executing the code. Then when you're ready, you can run your "execute" function, which takes the AST as input and actually does the steps it represents.

I'm not experienced enough with free monads to be able to write a complete tutorial on them, nor to directly answer your question with a step-by-step specific free-monad solution. But I can point you to a few resources that may help you understand the concepts behind them. First, the required Scott Wlaschin link:

https://fsharpforfunandprofit.com/posts/13-ways-of-looking-at-a-turtle-2/#way13

This is the last part of his "13 ways of looking at a turtle" series, where he builds a small LOGO-like turtle-graphics app using many different design styles. In #13, he uses the free-monad style, building it from the ground up so you can see the design decisions that go into that style.

Second, a set of links to Mark Seemann's blog. For the past month or two, Mark Seemann has been writing posts about the free-monad style, though I didn't realize that that's what he was writing about until he was several articles in. There's a terminology difference that may confuse you at first: Scott Wlaschin uses the terms "Stop" and "KeepGoing" for the two possible AST cases ("this is the end of the command list" vs. "there are more commands after this one"). But the traditional names for those two free-monad cases are "Pure" and "Free". IMHO, the names "Pure" and "Free" are too abstract, and I like Scott Wlaschin's "Stop" and "KeepGoing" names better. But I mention this so that when you see "Pure" and "Free" in Mark Seemann's posts, you'll know that it's the same concept as Scott Wlaschin's turtle example.

Okay, with that explanation finished, here are the links to Mark Seemann's posts:

Mark intersperses Haskell examples with F# examples, as you can tell from the URLs. If you are completely unfamiliar with Haskell, you can probably skip those posts as they might confuse you more than they help. But if you've got a passing familiarity with Haskell syntax, seeing the same ideas expressed in both Haskell and F# may help you grasp the concepts better, so I've included the Haskell posts as well as the F# posts.

As I said, I'm not quite familiar enough with free monads to be able to give you a specific answer to your question. But hopefully these links will give you some background knowledge that can help you implement what you're looking for.

rmunn
  • 34,942
  • 10
  • 74
  • 105