F# Monad 多个参数

F# Monad multiple parameters

我正在努力思考 monad 以及如何在现实世界的示例中使用它们。我给自己设定的第一个 "task" 是写一个 "Exception Monad",当然(在这一点上)只不过是为了满足我的目的而扭曲的 "Either monad"。

我的代码如下所示:

type MException<'a> = 
    | Success of 'a
    | Failure of string

    with
    static member returnM a =
        Success a

    static member bind f =
        fun e ->
            match e with
            | Success a -> f a
            | Failure m -> Failure m

    static member map f =
        fun e ->
            match e with
            | Success a -> Success (f a)
            | Failure m -> Failure m

// Create a little test case to test my code
let divide (n, m) =
    match m with
    | 0 -> Failure "Cannot divide by zero"
    | _ -> Success ((float n) / (float m))

let round (f:float) =
    Success ( System.Math.Round(f, 3) )

let toString (f:float) =
    sprintf "%f" f

let divideRoundAndPrintNumber = 
    divide
    >> MException<_>.bind round 
    >> MException<_>.map toString

// write the result
let result = divideRoundAndPrintNumber (11, 3)
match result with
| Success r -> printf "%s\n" r
| Failure m -> printf "%s\n" m

我的问题如下:除法函数现在接受一个元组。我可以或应该做些什么来使绑定和映射函数对具有多个参数的函数正确运行?

编辑 2015 年 12 月 30 日: @Mark Seemann 的回答和评论都有助于找到问题的答案。 @Mikhail 提供了解决方案的实现。柯里化是解决问题的正确方法。计算表达式不是解决方案,而是一种语法抽象,它确实有效,但一旦您向问题添加异步和其他模式,就会变得复杂。 "Simple" 组合似乎是最简单的 "trueest" 解决方案。

遗憾的是,我对 F# 的了解还不够,无法完全理解您的代码。例如,我不理解 >> 运算符和 MException<_> 表达式。但我可以为您的问题提供替代解决方案。它利用了一个名为 "Computation Expressions" 的 F# 功能。它使您能够以类似 F# 的方式施展 "Monadic" 魔法:

type MException<'a> = 
    | Success of 'a
    | Failure of string

type ExceptionBuilder() =

    member this.Bind (m, f) =
        match m with
        | Success a -> f a
        | Failure m -> Failure m

    member this.Return (x) =
        Success (x)

let ex = new ExceptionBuilder()

let divide n m =
    if m = 0 then Failure "Cannot divide by zero"
             else Success ((float n)/(float m))

let round (f : float) =
    Success (System.Math.Round(f, 3))

let divideRoundAndPrintNumber a b =
    ex {
        let! c = divide a b
        let! d = round c
        printf "result of divideRoundAndPrintNumber: %f\n" d
        return d
    }

let result = divideRoundAndPrintNumber 11 0
match result with
| Success r -> printf "%f\n" r
| Failure m -> printf "%s\n" m

抱歉,我的回答与您的问题不完全吻合,但希望对您有所帮助。

在这里您可以找到关于此主题的优秀博客 post 系列:

http://fsharpforfunandprofit.com/posts/computation-expressions-intro/

我也觉得这篇文章很有启发性:

http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html

Monad 具有相当严格的结构要求,它们必须具有:

Return: 'a -> m<'a>

绑定:m<'a> -> ('a -> m<'b>) -> m<'b>

您的除法函数具有签名 int*int -> MException<float>,即它确实具有与绑定一起使用所需的 'a -> m<'b> 形式。当与 bind 一起使用时,它会作用于 MException<int*int> 类型的东西并产生 MException<float>.

如果 divide 而不是类型 int -> int -> MException<float>(即 'a -> 'b -> m<'c>'),我们不能直接将其与 bind 一起使用。我们可以做的是打开元组,然后一个一个地提供参数来创建一个具有正确形式的 lambda。

让我们添加一个额外的 Return 以便我们可以更清楚地看到在这些约束下处理函数的一些不同方法:

let divideTupled (n, m) =
    match m with
    | 0 -> Failure "Cannot divide by zero"
    | _ -> Success ((float n) / (float m))

let divideRoundAndPrintNumber n m =
    MException<_>.Return (n,m) 
    |> MException<_>.Bind divideTupled
    |> MException<_>.Bind round 
    |> MException<_>.Map toString

let divideCurried n m =
    match m with
    | 0 -> Failure "Cannot divide by zero"
    | _ -> Success ((float n) / (float m))

let divideRoundAndPrintNumber n m =
    MException<_>.Return (n,m) 
    |> MException<_>.Bind (fun (n,m) -> divideCurried n m)
    |> MException<_>.Bind round 
    |> MException<_>.Map toString

如 Olaf 所述,计算表达式为在 F# 中处理 monad 提供了一些不错的语法糖。

divideRoundAndPrintNumber 更改为函数而不是值

let divide n m =
    match m with
    | 0 -> Failure "Cannot divide by zero"
    | _ -> Success ((float n) / (float m))

let divideRoundAndPrintNumber n = 
    divide n
    >> MException<_>.bind round
    >> MException<_>.map toString

为什么不像平时那样定义 divide

let divide n m =
    match m with
    | 0 -> Failure "Cannot divide by zero"
    | _ -> Success ((float n) / (float m))

然后您可以像这样定义 divideRoundAndPrintNumber,同样以柯里化形式:

let divideRoundAndPrintNumber n m = 
    divide n m
    |> MException<_>.bind round 
    |> MException<_>.map toString

FSI ad-hoc 测试:

> let result = divideRoundAndPrintNumber 11 3;;    
val result : MException<string> = Success "3.667000"

> let result = divideRoundAndPrintNumber 11 0;;    
val result : MException<string> = Failure "Cannot divide by zero"