7

我正在尝试应用F# 中描述的免费 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导致一个函数调用,它接受 akey : string并返回一个obj
  • Insert导致一个接受obj并返回的函数调用unit

问题

我定义了两个操作SelectInsert除了Done终止计算:

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 的定义以在整个计算过程中使用不同的类型?

4

1 回答 1

5

正如 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.

有一些方法可以使这种类型变得安全,但我认为如果你解释你试图通过使用单子结构来实现什么,你会得到更好的答案。

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

于 2017-01-16T02:00:30.413 回答