6

Maybe monad 允许链接一组所有可能失败的操作符(通过返回 None),Some result如果每个子操作成功,或者None任何失败,最后返回。这是一个小的虚拟示例:

type MaybeBuilder() =
    member this.Return(x) = 
        Some x
    member this.Bind(m, f) = 
        match m with
        | Some x -> f x
        | None -> None

let maybe = MaybeBuilder()

let list = [1;2;3;4]

// evaluates to Some 3
maybe {
    let! x1 = List.tryFind ((=) 1) list
    let! x2 = List.tryFind ((=) 2) list
    return x1 + x2
}

// evaluates to None 
maybe {
    let! x1 = List.tryFind ((=) 1) list
    let! x2 = List.tryFind ((=) 6) list
    return x1 + x2
}

这大致相当于:

// evaluates to Some 3
match List.tryFind ((=) 1) list with
| None -> None
| Some x1 ->
    match List.tryFind ((=) 2) list with
    | None -> None
    | Some x2 -> Some (x1 + x2)

// evaluates to None
match List.tryFind ((=) 1) list with
| None -> None
| Some x1 ->
    match List.tryFind ((=) 6) list with
    | None -> None
    | Some x2 -> Some (x1 + x2)

在我的一段代码中,我目前正在做与此相反的操作,返回第一个成功的命中:

// evaluates to Some 1
match List.tryFind ((=) 1) list with
| Some x1 -> Some x1
| None ->
    match List.tryFind ((=) 2) list with
    | Some x2 -> Some x2
    | None -> None

// evaluates to Some 2
match List.tryFind ((=) 6) list with
| Some x1 -> Some x1
| None ->
    match List.tryFind ((=) 2) list with
    | Some x2 -> Some x2
    | None -> None

这是否也可以通过 monad 来获得良好的计算表达式语法?

4

5 回答 5

8

前段时间,我写了一篇在 F# 中实现命令式计算生成器的博客。例如,以下是一个返回 0 并且从不执​​行printfn语句的计算:

let test() = imperative {
  return 0
  printfn "after return!"
  return 1 }

我认为您的代码示例可以写成:

imperative { return! List.tryFind ((=) 1) list 
             return! List.tryFind ((=) 2) list }

它是如何工作的?

正如你所建议的(李也提到过),类型也基于option<'T>类型,与我使用延迟选项值的细微差别(这样你就可以在不评估它们的情况下组合计算),所以单子的类型类型实际上是:

type Imperative<'T> = unit -> option<'T>

计算构建器中的关键技巧是添加Combine(其行为类似于mplusLee 的 Haskell 版本)运行第一个计算并返回其结果(如果它是Some)或运行其余的(如果它是None)(两者a实际上b都是函数 -所以我们需要调用它们并延迟结果):

member x.Combine(a, b) = (fun () ->
  match a() with 
  | Some(v) -> Some(v) // if it returned, we can return the result immediately
  | _ -> b() )         // otherwise, we need to run the second part

它实际上工作得非常好 - 您可以添加对循环和异常处理的支持,如果您使类型更复杂,您可以添加其他功能,例如break

imperative { 
  for x in 1 .. 5 do 
    if (x % 2 = 0) then do! continue
    printfn "number = %d" x }
于 2013-10-15T15:05:43.970 回答
6

托马斯的解决方案

imperative { 
    return! List.tryFind ((=) 1) list
    return! List.tryFind ((=) 2) list }

做我想做的事,但我刚刚意识到,我也可以通过以下方式更简单地实现我所需要的:

// evaluates to Some 1
[1;2] |> List.tryPick (fun x -> List.tryFind ((=) x) list)
// evaluates to Some 2
[6;2] |> List.tryPick (fun x -> List.tryFind ((=) x) list)
于 2013-10-15T15:37:21.783 回答
5

Haskell 使用MonadPlus定义为的类型类执行此操作:

class Monad m => MonadPlus m where
  mzero :: m a
  mplus :: m a -> m a -> m a

Maybe将此类型类实现为

instance MonadPlus Maybe where
  mzero                   = Nothing
  Nothing `mplus` Nothing = Nothing
  Just x  `mplus` Nothing = Just x
  Nothing `mplus` Just x  = Just x
  Just x  `mplus` Just y  = Just x

似乎mzeroand的mplus成员MonadPlus对应于F# 计算表达式使用的Zeroand成员。Combine

于 2013-10-15T15:05:28.303 回答
4

您还可以定义一个简单的函数来执行此操作:

let orElse f = function
  | None -> f()
  | Some _ as x -> x

您可以将任意数量的函数链接在一起,并将第一个Some结果作为整个表达式的结果返回:

List.tryFind ((=) 1) list
|> orElse (fun () -> List.tryFind ((=) 2) list)
|> orElse (fun () -> List.tryFind ((=) 3) list)
|> orElse (fun () -> List.tryFind ((=) 4) list)
于 2013-10-15T15:37:58.527 回答
1

这种特殊情况也可以用序列来模拟:

let tests = seq {
  yield List.tryFind ((=) 5) list
  yield List.tryFind ((=) 3) list
  yield List.tryFind ((=) 6) list
}

tests |> Seq.tryFind Option.isSome |> Option.bind id
于 2013-10-17T18:40:32.153 回答