F#中的组合单子

时间:2016-06-16 07:08:15

标签: f# functional-programming monads monad-transformers

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

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

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

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

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

3 个答案:

答案 0 :(得分:6)

写一个"合并"构建器将是你如何在F#中执行它,如果你要这样做。然而,这不是一种典型的方法,当然也不是一种实用方法。

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

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

答案 1 :(得分:5)

我将展示如何创建EitherWriter,根据您订购EitherWriter的方式,有两种方法可以构建其中一种,但我是将展示似乎最符合您所需工作流程的示例。

我也将简化编写器,使其仅记录到string list。更全面的编写器实现将使用memptymappend来抽象适当的类型。

类型定义:

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#for fun and profit有一些关于railway oriented programming的重要信息以及它与竞争方法相比带来的好处。

这些示例基于自定义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 monad的绑定函数。

我们可以像这样使用它:

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”; “错误:除以零”]

答案 2 :(得分:4)

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

#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和{{1与Choice2Of2一起使用它就可以了。

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