F#中的免费Monad,具有通用输出类型

时间:2017-01-15 23:59:54

标签: f# monads free-monad

我正在尝试应用F# for fun and profit中描述的免费monad模式来实现数据访问(用于Microsoft Azure表存储)

示例

假设我们有三个数据库表和三个dao的Foo,Bar,Baz:

Foo          Bar          Baz

key | col    key | col    key | col
---------    ---------    ---------
foo |  1     bar |  2         |

我想用key =“foo”选择Foo,使用key =“bar”选择Bar,用key =“baz”和col = 3

插入一个Baz
Select<Foo> ("foo", fun foo -> Done foo)
  >>= (fun foo -> Select<Bar> ("bar", fun bar -> Done bar)
    >>= (fun bar -> Insert<Baz> ((Baz ("baz", foo.col + bar.col), fun () -> Done ()))))

在解释器功能

  • Select会导致函数调用key : string并返回obj
  • Insert会导致函数调用obj并返回unit

问题

除了Select之外,我还定义了两个操作InsertDone来终止计算:

type StoreOp<'T> =
  | Select of string * ('T -> StoreOp<'T>)
  | Insert of 'T * (unit -> StoreOp<'T>)
  | Done of 'T

为了连接StoreOp,我试图实现正确的绑定功能:

let rec bindOp (f : 'T1 -> StoreOp<'T2>) (op : StoreOp<'T1>) : StoreOp<'T2> =
  match op with
  | Select (k, next) ->
      Select (k, fun v -> bindOp f (next v))
  | Insert (v, next) ->
      Insert (v, fun () -> bindOp f (next ()))
  | Done t ->
      f t

  let (>>=) = bindOp

然而,f#编译器正确警告我:

The type variable 'T1 has been constrained to be type 'T2

对于bindOp的这个实现,类型在整个计算过程中是固定的,所以代替:

Foo > Bar > unit

我所能表达的是:

Foo > Foo > Foo

在整个计算过程中,如何修改StoreOp和/或bindOp的定义以使用不同的类型?

1 个答案:

答案 0 :(得分:4)

正如Fyodor在评论中提到的,问题在于类型声明。如果你想以牺牲类型安全的代价进行编译,你可以在两个地方使用obj - 这至少可以显示出问题所在:

type StoreOp<'T> =
  | Select of string * (obj -> StoreOp<'T>)
  | Insert of obj * (unit -> StoreOp<'T>)
  | Done of 'T

我不完全确定这两个操作应该建模的内容 - 但我想Select表示您正在阅读某些内容(使用string密钥?)并且Insert表示您正在存储一些值(然后继续unit)。所以,在这里,您存储/阅读的数据将是obj

有一些方法可以使这种类型安全,但我认为如果你通过使用monadic结构解释了你想要实现的目标,你会得到更好的答案。

我不知道更多,我认为使用免费monad只会让你的代码变得非常混乱和难以理解。 F#是一种功能优先的语言,这意味着您可以使用不可变数据类型以良好的功能样式编写数据转换,并使用命令式编程来加载数据并存储结果。如果您正在使用表存储,为什么不编写正常的命令式代码来从表存储中读取数据,将结果传递给纯函数转换然后存储结果?