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