我正试图在F#中找到monads,我正在寻找一个组合它们的例子。
在haskell中看起来你会使用Monad Transformers但在F#中看起来你会创建自己的计算表达式构建器。
我可以支持这一点,但有没有标准monad的一些组合的例子以及如何使用它们?
我特别感兴趣的是将Reader,Writer和Either结合起来构建接受环境的函数,调整它,然后使用Writer将更改返回到发生的环境中。两者都可用于区分成功与失败。
现在,获得一个产生值+ log或错误的EitherWriter计算表达式的例子会很棒。
我将展示如何创建一个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”; “错误:除以零”]
编写“组合”构建器将是您在F#中执行此操作的方式 如果 你是这样做的。然而,这不是典型的方法,当然也不是实用的方法。
在Haskell中,你需要monad变换器,因为Haskell中的monad无处不在。 F#不是这种情况 - 这里计算工作流程是一个有用的工具,但只是一个补充工具。首先 - F#并没有禁止副作用,因此使用monad的一个重要原因就是消失了。
典型的方法是确定捕获您想要建模的计算本质的工作流程(在您的情况下,它似乎是Either monad)并使用其他方法来完成剩下的工作 - 比如线程化修改后的“环境”通过计算作为值或使用副作用进行日志记录(又名“日志框架”)。
我知道它在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
它会起作用。
就个人而言,我更喜欢首先使用这种方法,它更容易编写,当然也更短。一旦我拥有了所需的功能,我就可以自定义我的变压器,或者保持原样,如果它足够好我想要解决的问题。