robkuz
robkuz

Reputation: 9934

Can I create nested Computation Expressions for Builder Like DSLs?

This is what I'd like to do:

type DirectionBuilder() =
    member self.Yield(()) = []

    [<CustomOperation("left")>]
    member self.Left (acc, degree) = None

    [<CustomOperation("right")>]
    member self.Right (acc, degree) = None 

    [<CustomOperation("velocity")>]
    member self.Velocity (acc, ()) = new VelocityBuilder()


and VelocityBuilder() = 
    member self.Yield(()) = []

    [<CustomOperation("accelerate")>]
    member self.Accesslarate (acc, v) = None

    [<CustomOperation("decelerate")>]
    member self.Decelerate (acc, v) = None


let direction () = new DirectionBuilder()

direction() {
    right 90
    left 30
    velocity() {
        accelerate 1
    }
}

This breaks down at line

      velocity() {
  ----^^^^^^^^
  stdin(952,5): error FS3099: 'velocity' is used with an incorrect number of 
  arguments. This is a custom operation in this query or computation 
  expression. Expected 1 argument(s), but given 2.
  > 

Is there anyway to explain to F# that this custom operation should indeed accept a computation expression?

Upvotes: 4

Views: 866

Answers (1)

scrwtp
scrwtp

Reputation: 13577

I suppose there is a way to almost get the syntax you want. You make velocity operation accept a type that the VelocityBuilder produces - which in your case appears to be an option. Then you create a separate computation and pass it in.

So you get something like this:

type DirectionBuilder() =
    member self.Yield(()) = []

    [<CustomOperation("left")>]
    member self.Left (acc, degree) = None

    [<CustomOperation("right")>]
    member self.Right (acc, degree) = None 

    [<CustomOperation("velocity")>]
    member self.Velocity (acc, odd: 'a option) = None


type VelocityBuilder() = 
    member self.Yield(()) = []

    [<CustomOperation("accelerate")>]
    member self.Accelerate (acc, v) = None

    [<CustomOperation("decelerate")>]
    member self.Decelerate (acc, v) = None

let dir = new DirectionBuilder()
let vel = new VelocityBuilder()

dir {
    right 90
    left 30
    velocity (vel {
        accelerate 1
    })
}

That said, if you set out to write computation workflows, you probably should start by designing a type to represent the state of your computation. Right now you have syntax sugar, but no meat ;)

Once you have a type, a workflow can follow from that if it turns out to be useful.

Upvotes: 2

Related Questions