问题 F#中的组合单子


我正试图在F#中找到monads,我正在寻找一个组合它们的例子。

在haskell中看起来你会使用Monad Transformers但在F#中看起来你会创建自己的计算表达式构建器。

我可以支持这一点,但有没有标准monad的一些组合的例子以及如何使用它们?

我特别感兴趣的是将Reader,Writer和Either结合起来构建接受环境的函数,调整它,然后使用Writer将更改返回到发生的环境中。两者都可用于区分成功与失败。

现在,获得一个产生值+ log或错误的EitherWriter计算表达式的例子会很棒。


7861
2018-06-16 07:08


起源

IIRC, fsprojects.github.io/FSharpx.Extras 有一些。我在这里也举了一个例子: blog.ploeh.dk/2016/04/11/async-as-surrogate-io - Mark Seemann
这是一个 组合Reader,Writer和State monads的示例 在F#中。一个 替代 做你所描述的方式 可以 通过这个 使用代数效果和处理程序的示例 在F#中。 - Nikos Baxevanis
我没有你正在寻找的东西但是这个小项目使用了一个组合的阅读器/状态monad: github.com/TheInnerLight/RemoteMonad 它是本文中一些(Haskell)代码的F#转换: cs.rhul.ac.uk/home/ucac009/publications/remote-monad.pdf  另外,我正在开发的IO库(对于F#中的monadic IO)实现了许多更复杂的monadic操作,例如monadic循环,并且有很好的文档记录: github.com/TheInnerLight/NovelIO - TheInnerLight
我使用的是F#monad变换器 FSharpPlus 快速原型化我正在寻找的东西,然后,有一个工作的例子,我可以保持原样或自定义转换后的monad。顺便说一下有很多变形金刚使用 Async<'T> 代替Haskell的 IO<'T> 正如@MarkSeemann在他的链接博客文章中指出的那样。 - Gustavo


答案:


我将展示如何创建一个EitherWriter,有两种方法可以构建其中一种,具体取决于您如何订购 Either 和 Writer 但我将展示似乎最符合您所需工作流程的示例。

我也将简化编写器,使其只记录到一个 string list。更全面的编写器实现将使用 mempty 和 mappend 抽象适当的类型。

类型定义:

type EitherWriter<'a,'b>  = EWriter of string list * Choice<'a,'b>

基本功能:

let runEitherWriter = function
    |EWriter (st, v) -> st, v

let return' x = EWriter ([], Choice1Of2 x)

let bind x f =
    let (st, v) = runEitherWriter x
    match v with
    |Choice1Of2 a -> 
        match runEitherWriter (f a) with
        |st', Choice1Of2 a -> EWriter(st @ st', Choice1Of2 a)
        |st', Choice2Of2 b -> EWriter(st @ st', Choice2Of2 b)
    |Choice2Of2 b -> EWriter(st, Choice2Of2 b)

我喜欢在独立模块中定义它们,然后我可以直接使用它们或引用它们来创建计算表达式。同样,我将保持简单,只做最基本的可用实现:

type EitherWriterBuilder() =
    member this.Return x = return' x
    member this.ReturnFrom x = x
    member this.Bind(x,f) = bind x f
    member this.Zero() = return' ()

let eitherWriter = EitherWriterBuilder()

这有什么用的吗?

F#的乐趣和利润有一些很好的信息 铁路导向编程 以及它与竞争方法相比带来的优势。

这些示例基于自定义 Result<'TSuccess,'TFailure> 但是,当然,它们同样可以使用F#的内置功能 Choice<'a,'b> 类型。

虽然我们可能会遇到以这种面向铁路的形式表达的代码,但我们更不可能遇到预先编写的代码,可以直接使用 EitherWriter。因此,该方法的实用性取决于从简单的成功/失败代码到与上述monad兼容的容易的转换。

以下是成功/失败功能的示例:

let divide5By = function
    |0.0 -> Choice2Of2 "Divide by zero"
    |x -> Choice1Of2 (5.0/x)

此功能只用提供的数字除以5。如果该数字为非零,则返回包含结果的成功,如果提供的数字为零,则返回失败告诉我们我们已尝试除以零。

我们现在需要一个辅助函数来将这样的函数转换为可用于我们的函数 EitherWriter。可以做到这一点的功能是这样的:

let eitherConv logSuccessF logFailF f = 
    fun v ->
        match f v with
        |Choice1Of2 a -> EWriter(["Success: " + logSuccessF a], Choice1Of2 a)
        |Choice2Of2 b -> EWriter(["ERROR: " + logFailF b], Choice2Of2 b)

它需要一个描述如何记录成功的函数,一个描述如何记录失败的函数和一个绑定函数 Either monad和它返回一个绑定函数 EitherWriter 单子。

我们可以像这样使用它:

let ew = eitherWriter {
    let! x = eitherConv (sprintf "%f") (sprintf "%s") divide5By 6.0
    let! y = eitherConv (sprintf "%f") (sprintf "%s") divide5By 3.0
    let! z = eitherConv (sprintf "%f") (sprintf "%s") divide5By 0.0
    return (x, y, z)
}

let (log, _) = runEitherWriter ew

printfn "%A" log

然后返回:

[“成功:0.833333”; “成功:1.666667”; “错误:除以零”]


5
2018-06-17 20:55



有没有使用mappend / mempty或某种界面来定义它们的例子? - Beau Trepp
@ user1570690这里有一个抽象类示例: github.com/fsprojects/FSharpx.Extras/blob/master/src/... - TheInnerLight
相反的情况呢,铁路导向的monad是状态+对数还是失败? - Beau Trepp
@ user1570690在这种情况下,您需要一个带有两个联合案例的自定义类型,一个用于成功,另一个用于成功案例中包含的值和状态失败。然后,您需要一个与该类型匹配的绑定函数。 - TheInnerLight


编写“组合”构建器将是您在F#中执行此操作的方式 如果 你是这样做的。然而,这不是典型的方法,当然也不是实用的方法。

在Haskell中,你需要monad变换器,因为Haskell中的monad无处不在。 F#不是这种情况 - 这里计算工作流程是一个有用的工具,但只是一个补充工具。首先 - F#并没有禁止副作用,因此使用monad的一个重要原因就是消失了。

典型的方法是确定捕获您想要建模的计算本质的工作流程(在您的情况下,它似乎是Either monad)并使用其他方法来完成剩下的工作 - 比如线程化修改后的“环境”通过计算作为值或使用副作用进行日志记录(又名“日志框架”)。


6
2018-06-16 08:40





我知道它在F#中通常不被认为是惯用的,但对于好奇的读者来说,这里是@TheInnerLight的答案 F#+ :

#r @"FSharpPlus.1.0.0-CI00089\lib\net40\FSharpPlus.dll"

open FSharpPlus

let divide5By = function
    |0.0 -> Choice2Of2 "Divide by zero"
    |x   -> Choice1Of2 (5.0/x)

let eitherConv logSuccessF logFailF f v = 
    ErrorT (
        match f v with
        | Choice1Of2 a -> Writer(Choice1Of2 a, ["Success: " + logSuccessF a])
        | Choice2Of2 b -> Writer(Choice2Of2 b, ["ERROR: "   + logFailF b]  ))

let ew = monad {
    let! x = eitherConv (sprintf "%f") (sprintf "%s") divide5By 6.0
    let! y = eitherConv (sprintf "%f") (sprintf "%s") divide5By 3.0
    let! z = eitherConv (sprintf "%f") (sprintf "%s") divide5By 0.0
    return (x, y, z)
}

let (_, log) = ew |> ErrorT.run |> Writer.run

当然,这个适用于任何幺半群。

这种方法基本上是Haskell方法,变换器适用于任何monad,并且在上面的代码中你可以轻松切换到 OptionT,替换 Choice1Of2 同 Some 和 Choice2Of2 同 None 它会起作用。

就个人而言,我更喜欢首先使用这种方法,它更容易编写,当然也更短。一旦我拥有了所需的功能,我就可以自定义我的变压器,或者保持原样,如果它足够好我想要解决的问题。


4
2018-06-18 18:23



这肯定是获得可行解决方案的更快捷方式! - TheInnerLight