Как создать тип DSL для чередующихся вызовов функций

Я хочу создать DSL, где функции 2 (foo и bar) могут быть вызваны последовательно, так что

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

это работает довольно хорошо, определяя

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)

Теперь, однако, я хочу также разрешить выполнение нескольких вызовов bar, но foo еще нужно вызывать только один раз

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

В С# я могу перегрузить bar, чтобы принять BarResult или FooResult, но это не работает для F #. По крайней мере, не легко. Я также попытался создать несколько Дискриминационных Союзов, но я действительно не могу обойти его.

Ответ 1

Это интересный вопрос!

Существующий код работает очень хорошо, но я бы сделал одно изменение - вам фактически не нужно передавать фактические значения FooResult и BarResult. Вы можете определить тип MarkedType<'TPhantom, 'TValue>, который представляет значение 'TValue со специальной "меткой", указанной другим типом:

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

Затем вы можете использовать интерфейсы как параметры типа для типа phantom. Мне было сложно подумать о "результатах", поэтому я буду использовать входы вместо:

type IFooInput = interface end
type IBarInput = interface end

Теперь уловка заключается в том, что вы также можете определить интерфейс, который является как IFooInput, так и IBarInput:

type IFooOrBarInput =
  inherit IFooInput
  inherit IBarInput

Итак, теперь вам нужно добавить соответствующие аннотации к foo и bar:

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

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

Здесь аннотация на входе говорит, что она должна принимать все, что есть или наследуется от IFooInput или IBarInput. Но результат функции bar отмечен IFooOrBarInput, что позволяет передать его как на foo, так и на bar:

(Value 0 : MarkedValue<IFooInput, _>)
|> foo 10
|> bar "A"
|> bar "A"
|> foo 20
|> bar "B"