Skip to content

Instantly share code, notes, and snippets.

@Krzysztof-Cieslak
Created November 19, 2019 15:36
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Krzysztof-Cieslak/746b813ac3020d685cc1446354bf404c to your computer and use it in GitHub Desktop.
Save Krzysztof-Cieslak/746b813ac3020d685cc1446354bf404c to your computer and use it in GitHub Desktop.
Using F# 4.7 implicit yield feature to implement DSL in F#
//Builder represeting only one entity in DSL
type Child1State = {SomeState : string}
type Child1Builder () =
[<CustomOperation("set_state")>]
member __.SetState (st, x) = {st with SomeState = x}
member __.Yield _ = {SomeState = ""}
let child = Child1Builder ()
//Builder represeting another entity in DSL
type Child2State = {DifferentState : string; MoreState: int}
type Child2Builder () =
[<CustomOperation("set_different_state")>]
member __.SetDifferent (st, x) = {st with DifferentState = x}
[<CustomOperation("set_more_state")>]
member __.SetOther (st, x) = {st with MoreState = x}
member __.Yield _ = {DifferentState = ""; MoreState = 0}
let differentChild = Child2Builder ()
//Another builder representing part of DSL
type NameBuilderState = {State: string}
type NameBuilder () =
member __.Yield (str: string) = {State = str}
member __.Zero () = {State = ""}
member __.Delay f = f()
member __.Combine(st, ns) = ns
let name = NameBuilder ()
//Top level builder, that has it's own state, and embeds child entities
type MainBuilderState = {Name : string option; ChildState : Child1State option; OtherChildState: Child2State option }
type MainBuilder () =
member __.Yield (es: Child1State) =
{Name = None; ChildState = Some es; OtherChildState = None}
member __.Yield (es: Child2State) =
{Name = None; ChildState = None; OtherChildState = Some es}
member __.Yield (es: NameBuilderState) =
{Name = Some es.State; ChildState = None; OtherChildState = None}
member __.Yield (_: unit) =
{Name = None; ChildState = None; OtherChildState = None}
member __.Combine(st: MainBuilderState, ns: MainBuilderState) =
let ms = st.Name |> Option.orElse ns.Name
let c1 = st.ChildState |> Option.orElse ns.ChildState
let c2 = st.OtherChildState |> Option.orElse ns.OtherChildState
{Name = ms; ChildState = c1; OtherChildState = c2}
// This doesn't work, no idea why?
// [<CustomOperation("nnn")>]
// member __.Name (st, x) = {st with Name = Some x}
member __.Delay f = f()
member __.Zero () = {Name = None; ChildState = None; OtherChildState = None}
let main = MainBuilder ()
//Final sample
let sample = main {
name {"My name" }
child {
set_state "Test"
}
differentChild {
set_different_state "Other test"
set_more_state 123
}
}
@baronfel
Copy link

you can make nnn work by adding a simple 'for' overload:

    member __.For(st, func) = 
      __.Delay(fun () -> func ())

@Krzysztof-Cieslak
Copy link
Author

/facepalm

I’ve seen error message about missing For but from my Saturn experience I remember that it’s usually “fallback” error when anything is wrong with CE and assumed something else is needed.

Silly me

@baronfel
Copy link

I'm not actually sure that my For implementation there is legitimate, but the result of using it does set names that I specify, so I guess it's good?

@isaacabraham
Copy link

@baronfel I have no idea what that syntax means but yes, it works!

@isaacabraham
Copy link

@baronfel one thing I've noticed is Combine doesn't seem to get called after a custom operation gets called e.g.

let sample = main {
    nnn "ISAAC"
    differentChild {
        set_different_state "Other test"
        set_more_state 123
    }
}

creates the following object

  { Name = None
    ChildState = None
    OtherChildState = Some { DifferentState = "Other test"
                             MoreState = 123 } }

If you put nnn at the end of the computation expression, it then works.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment