Fede
Fede

Reputation: 44038

Computation Expression for constructing complex object graph

Given the following types:

type Trip = {
  From: string
  To: string
}

type Passenger = {
   Name: string
   LastName: string
   Trips: Trip list
}

I'm using the following builders:

type PassengerBuilder() = 
  member this.Yield(_) = Passenger.Empty

  [<CustomOperation("lastName")>]
  member __.LastName(r: Passenger, lastName: string) = 
    { r with LastName = lastName }

  [<CustomOperation("name")>]
  member __.Name(r: Passenger, name: string) = 
    { r with Name = name }

type TripBuilder() = 
  member __.Yield(_) = Trip.Empty

  [<CustomOperation("from")>]
  member __.From(t: Trip, f: string) = 
    { t with From = f }

  // ... and so on

to create records of type Passenger, like so:

let passenger = PassengerBuilder()
let trip = TripBuilder()

let p = passenger {
  name "john"
  lastName "doe"
}

let t = trip {
  from "Buenos Aires"
  to "Madrid"
}

how would I go about combining the PassengerBuilder and the TripBuilder so that I can achieve this usage?

let p = passenger {
    name "John"
    lastName "Doe"
    trip from "Buenos Aires" to "Madrid"
    trip from "Madrid" to "Paris"
}

that returns a Passenger record like:

{
   LastName = "Doe"
   Name = "John"
   Trips = [
       { From = "Buenos Aires"; To = "Madrid" }
       { From = "Madrid"; To = "Paris" }
   ]
}

Upvotes: 2

Views: 123

Answers (2)

Tomas Petricek
Tomas Petricek

Reputation: 243061

Is there any reason why you want to use computation expression builder? Based on your example, it does not look like you're writing anything computation-like. If you just want a nice DSL for creating trips, then you could quite easily define something that lets you write:

let p = 
  passenger [
    name "John"
    lastName "Doe"
    trip from "Buenos Aires" towards "Madrid"
    trip from "Madrid" towards "Paris"
  ]

This is pretty much exactly what you asked for, except that it uses [ .. ] instead of { .. } (because it creates a list of transformations). I also renamed to to towards because to is a keyword and you cannot redefine it.

The code for this is quite easy to write and follow:

let passenger ops = 
  ops |> List.fold (fun ps op -> op ps)
    { Name = ""; LastName = ""; Trips = [] } 

let trip op1 arg1 op2 arg2 ps = 
  let trip = 
    [op1 arg1; op2 arg2] |> List.fold (fun tr op -> op tr)
      { From = ""; To = "" }
  { ps with Trips = trip :: ps.Trips }

let name n ps = { ps with Name = n }
let lastName n ps = { ps with LastName = n }
let from n tp = { tp with From = n }
let towards n tp = { tp with To = n }

That said, I would still consider using normal F# record syntax - it is not that much uglier than this. The one drawback of the version above is that you can create passengers with empty names and last names, which is one thing that F# prevents you from!

Upvotes: 5

Fyodor Soikin
Fyodor Soikin

Reputation: 80744

I'm not sure this is what you wanted, but nothing prevents you from creating a new operation called trip on your PassengerBuilder:

  [<CustomOperation("trip")>]
  member __.Trip(r: Passenger, t: Trip) = 
    { r with Trips = t :: r.Trips }

and then using it like this:

let p = passenger {
    name "John"
    lastName "Doe"
    trip (trip { from "Buenos Aires"; to "Madrid" })
    trip (trip { from "Madrid"; to "Paris" })
}

Arguably, you can even make it cleaner by dropping the TripBuilder altogether:

let p = passenger {
    name "John"
    lastName "Doe"
    trip { From = "Buenos Aires"; To = "Madrid" }
    trip { From = "Madrid"; To = "Paris" }
}

If this is somehow not what you wanted, then please specify how. That is, what is missing or what is extra in this solution.

Upvotes: 4

Related Questions