I would like to construct some objects using Computation Expression syntax. Ideally, I’d like to have one Computation Expression, and have the compiler figure out the right set of overloads to make the types work.
The idea is to have a builder where the user writes two sections:
program {
// Input section
// Read inputs (statically analyzable)
let! x = Input.read "x"
and! y = Input.read "y"
// ...
// Instruction section
// Take actions
// ...
do! InstrFree.show message
// Result section
return x + y
}
This is possible using plain functions, but I want to see what syntax is possible.
Below is a self-contained example that does not compile.
Is it possible to tweak the builder object so that the example works?
type Input<'a> =
{
Keys : Set<string>
TryRead : Map<string, obj> -> 'a option
}
module Input =
let read (key : string) : Input<'a> =
{
Keys = Set.singleton key
TryRead =
fun m ->
match Map.tryFind key m with
| Some (:? 'a as a) -> Some a
| _ -> None
}
let map2 (f : 'a -> 'b -> 'c) (x : Input<'a>) (y : Input<'b>) =
{
Keys = Set.union x.Keys y.Keys
TryRead =
fun m ->
match x.TryRead m with
| Some a ->
match y.TryRead m with
| Some b -> Some (f a b)
| None -> None
| None -> None
}
type Instr<'a> =
| Load of string * (string -> 'a)
| Show of string * (unit -> 'a)
module Instr =
let map (f : 'a -> 'b) (x : Instr<'a>) : Instr<'b> =
match x with
| Load (key, unpack) -> Load (key, unpack >> f)
| Show (key, unpack) -> Show (key, unpack >> f)
type InstrFree<'a> =
| Just of 'a
| Free of Instr<InstrFree<'a>>
module InstrFree =
let just x =
Just x
let rec bind (f : 'a -> InstrFree<'b>) (x : InstrFree<'a>) : InstrFree<'b> =
match x with
| Just a -> f a
| Free instr ->
Instr.map (bind f) instr
|> Free
let show (message : string) =
InstrFree.Free (Instr.Show (message, fun () -> Just ()))
type Program<'i, 'o> =
{
Input : Input<'i>
Output : 'i -> InstrFree<'o>
}
module Program =
let chain (x : Program<'i, 'j>) (y : Program<'j, 'o>) : Program<'i, 'o> =
{
Input = Input.map2 (fun a _ -> a) x.Input y.Input
Output =
fun i ->
let f = x.Output i
f
|> InstrFree.bind (fun j -> y.Output j)
}
type ProgramBuilder() =
member this.MergeSources(x, y) =
Input.map2 (fun a b -> a, b) x y
member this.BindReturn(x, y) =
{
Input = x
Output = y
}
member this.BindReturn(x, f) =
{
Input = x
Output = fun i -> InstrFree.just (f i)
}
member this.Bind(x, f) =
{
Input = x.Input
Output =
fun i ->
let a = x.Output i
a |> InstrFree.bind f
}
member this.Combine(a, b) =
Program.chain a b
let program = ProgramBuilder()
let a =
program {
let! x = Input.read "x"
return x + 1
}
// Does not compile
let b =
program {
let! x = Input.read "x"
and! y = Input.read "y"
return x * y
}
// Does not compile
let c =
program {
let! x = Input.read "x"
and! y = Input.read "y"
let message = $"x + y = %i{x + y}"
do! InstrFree.show message
}
Sorry the example is quite long; I was unable to come up with a smaller example!
It follows the design pattern set out here.
It is possible to have the compiler choose between overloads in computational expressions, by providing more than one .Source
member in the builder class.
Unfortunately this does not seem to be documented in the Microsoft documentation.
You can see an example here:
https://github.com/demystifyfp/FsToolkit.ErrorHandling/blob/master/src/FsToolkit.ErrorHandling/AsyncResultCE.fs
In this case, the AsyncResult builder can bind (using let!
) on many types, including Async<'a>
, Result<'a,'e>
, etc.
Happy coding,
Roland
Fixes for the above:
type Input<'a> =
{
Keys : Set<string>
TryRead : Map<string, obj> -> 'a option
}
module Input =
let constant x =
{
Keys = Set.empty
TryRead =
fun _ -> Some x
}
let read (key : string) : Input<'a> =
{
Keys = Set.singleton key
TryRead =
fun m ->
match Map.tryFind key m with
| Some (:? 'a as a) -> Some a
| _ -> None
}
let map (f : 'a -> 'b) (x : Input<'a>) : Input<'b> =
{
Keys = x.Keys
TryRead =
fun m ->
x.TryRead m
|> Option.map f
}
let map2 (f : 'a -> 'b -> 'c) (x : Input<'a>) (y : Input<'b>) =
{
Keys = Set.union x.Keys y.Keys
TryRead =
fun m ->
match x.TryRead m with
| Some a ->
match y.TryRead m with
| Some b -> Some (f a b)
| None -> None
| None -> None
}
let int (key : string) : Input<int> =
read key
type Instr<'a> =
| Load of string * (string -> 'a)
| Show of string * (unit -> 'a)
module Instr =
let map (f : 'a -> 'b) (x : Instr<'a>) : Instr<'b> =
match x with
| Load (key, unpack) -> Load (key, unpack >> f)
| Show (key, unpack) -> Show (key, unpack >> f)
type InstrFree<'a> =
| Just of 'a
| Free of Instr<InstrFree<'a>>
module InstrFree =
let just x =
Just x
let rec map (f : 'a -> 'b) (x : InstrFree<'a>) : InstrFree<'b> =
match x with
| Just a -> Just (f a)
| Free instr ->
Instr.map (map f) instr
|> Free
let rec bind (f : 'a -> InstrFree<'b>) (x : InstrFree<'a>) : InstrFree<'b> =
match x with
| Just a -> f a
| Free instr ->
Instr.map (bind f) instr
|> Free
let show (message : string) =
InstrFree.Free (Instr.Show (message, fun () -> Just ()))
type Program<'a> =
{
Input : Input<obj>
Output : obj -> InstrFree<'a>
}
module Program =
let make (input : Input<'i>) (f : 'i -> InstrFree<'o>) : Program<'o> =
{
Input = input |> Input.map box
Output =
fun o ->
let i = unbox<'i> o
f i
}
let map (f : 'a -> 'b) (x : Program<'a>) : Program<'b> =
{
Input = x.Input
Output = fun i -> x.Output i |> InstrFree.map f
}
let bind (f : 'a -> InstrFree<'b>) (x : Program<'a>) : Program<'b> =
{
Input = x.Input
Output =
fun o ->
let m = x.Output o
InstrFree.bind f m
}
let ofInput (x : Input<'a>) : Program<'a> =
make x InstrFree.just
let ofInstrFree (x : InstrFree<'a>) : Program<'a> =
make
(Input.constant ())
(fun () -> x)
type ProgramBuilder() =
member this.MergeSources(x, y) =
Input.map2 (fun a b -> a, b) x y
member this.Source(x : Input<'a>) =
x
member this.Source(x : InstrFree<'a>) =
x
member this.Zero() =
InstrFree.just ()
member this.Bind(x, f) =
InstrFree.bind f x
member this.Bind(x, f) =
Program.make x f
member this.BindReturn(x, f) =
Input.map f x
member this.ReturnFrom(x : InstrFree<'a>) =
x
member this.Run(x : Program<'a>) =
x
member this.Run(x : Input<'a>) =
Program.ofInput x
member this.Run(x : InstrFree<'a>) =
Program.ofInstrFree x
let program = ProgramBuilder()
let a =
program {
let! x = Input.int "x"
return x + 1
}
let b =
program {
let! x = Input.int "x"
and! y = Input.int "y"
return x * y
}
let c =
program {
do! InstrFree.show "Hello, world."
}
let d =
program {
let! x = Input.int "x"
let message = $"x = %i{x}"
do! InstrFree.show message
}
let e =
program {
let! x = Input.read "x"
and! y = Input.read "y"
let message = $"x + y = %i{x + y}"
do! InstrFree.show message
}
let f =
program {
()
}
let g =
program {
return! InstrFree.show "Hello, world."
}
let h =
program {
do! InstrFree.show "a"
do! InstrFree.show "b"
do! InstrFree.show "c"
}
let i =
program {
do! InstrFree.show "a"
do! InstrFree.show "b"
do! InstrFree.show "c"
return! InstrFree.show "x"
}