Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a typesafe DSL for interleaved function calls

Tags:

f#

dsl

I want to create a DSL where 2 (foo and bar) functions can be called in succession so that

initialize()
|> foo 10
|> bar "A"
|> foo 20
|> bar "B"
|> transform

this works pretty perfect by defining

type FooResult = FooResult
type BarResult = BarResult

let foo param (result_type:BarResult, result) = (FooResult, transform param result)
let bar param (result_type:FooResult, result) = (BarResult, transform param result)

Now however I want to also allow that multiple bar calls can be executed in succession however foos still have to be called only once

initialize()
|> foo 10
|> bar "A"
//OK
|> bar "B"
|> transform

initialize()
|> foo 10
|> bar "A"
|> foo 20
//should yield an compile error
|> foo 30
|> bar "B"
|> transform

In C# I could overload bar to either accept BarResult or FooResult but that doesnt work for F#. At least not easily. I also tried to create some Discriminate Unions but I really cant get my head around it.

like image 516
robkuz Avatar asked Jan 05 '15 16:01

robkuz


1 Answers

This is a fun question!

Your existing code works quite nicely, but I would do one change - you do not actually need to pass around actual FooResult and BarResult values. You can define a type MarkedType<'TPhantom, 'TValue> which represents a value of 'TValue with a special "mark" specified by the other type:

type MarkedValue<'TPhantom, 'TValue> = Value of 'TValue

Then you can use interfaces as type parameters for the phantom type. I found it a bit hard to think about the "results", so I'm going to use inputs instead:

type IFooInput = interface end
type IBarInput = interface end

The trick now is that you can also define an interface that is both IFooInput and IBarInput:

type IFooOrBarInput =
  inherit IFooInput
  inherit IBarInput

So, all you need ot do now is to add appropriate annotations to foo and bar:

let foo param (Value v : MarkedValue<#IFooInput, _>) : MarkedValue<IBarInput, _> = 
  Value 0

let bar param (Value v : MarkedValue<#IBarInput, _>) : MarkedValue<IFooOrBarInput, _> = 
  Value 0

Here, the annotation on the input says that it should accept anything that is or inherits from IFooInput or IBarInput. But the result of the bar function is marked with IFooOrBarInput, which makes it possible to pass it to both foo and bar:

(Value 0 : MarkedValue<IFooInput, _>)
|> foo 10
|> bar "A"
|> bar "A"
|> foo 20
|> bar "B"
like image 112
Tomas Petricek Avatar answered Nov 14 '22 04:11

Tomas Petricek