【问题标题】:Free Monad in F# with generic output typeF# 中具有通用输出类型的免费 Monad
【发布时间】:2017-05-30 18:43:05
【问题描述】:

我正在尝试应用 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

问题

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

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

【问题讨论】:

  • 我可以在您的 bindOp 代码中指出此错误的确切原因,但根本原因是您的 StoreOp 类型。如果你仔细观察,你会发现它只能表达同一类型的操作链。
  • 难道不能避免所有这些间接级别并在Transaction Script 中执行简单的 CRUD 操作吗?这与 Tomas Petricek 在其answer 的最后一段中所描述的相似。另见Why the free Monad isn't free
  • 当前实现是一组简单的命令式 CRUD 函数。请参阅下面的评论以获取动力。

标签: f# monads free-monad


【解决方案1】:

正如 Fyodor 在 cmets 中提到的,问题在于类型声明。如果你想让它以牺牲类型安全为代价编译,你可以在两个地方使用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# 是一种功能优先语言,这意味着您可以使用不可变数据类型以良好的功能样式编写数据转换,并使用命令式编程来加载数据和存储结果。如果您正在使用表存储,为什么不编写普通的命令式代码从表存储中读取数据,将结果传递给纯函数转换,然后存储结果?

【讨论】:

  • 感谢您的回答。回答您的问题:我正在尝试按照Purity in an impure language 中的说明区分纯代码和不纯代码。您提到有一些方法可以使“obj 解决方案”类型安全。您能分享一下您将采取的方法吗?
  • 我同意分离纯代码和不纯代码是可取的,但免费的 monad 是一种糟糕的方式。最后,代码无论如何都是不纯的——免费的 monad 让你做的是抽象出杂质是如何处理的——我认为这样做没有任何好处。 (您可能会争辩说这对测试很有用,但我认为它只是掩盖了您应该测试的关键部分的测试,那就是对数据的操作。)如果您想要实现特定的目标,那么会有更好的方法来做到这一点。
  • 对于一个类型安全的版本,你最终会得到一个静态跟踪所有读写类型的类型,例如M&lt;Read&lt;int,&lt;Write&lt;string,&lt;Read&lt;int, Return&lt;unit&gt;&gt;&gt;&gt;&gt;&gt;&gt;一个使用它的示例项目是:blumu.github.io/ResumableMonad跨度>
  • 有趣,我会试试看。免责声明:意见,轶事。过去我在使用免费 monad 方面有很好的经验,但是到目前为止我还没有遇到对通用输出类型的需求。可测试性是文献中经常提到的一个方面。此外,我喜欢这样一个事实,即实现细节,如使用OptionChoice、日志记录或本地状态都整齐地位于解释器函数中。不纯代码的执行也被推到应用程序执行的边界。目前我正在尝试将 CRUD 解决方案与免费的 monad 解决方案进行比较。
猜你喜欢
  • 2014-04-03
  • 2018-07-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多