Haskell 的绑定运算符 (>>=) 是否等同于 F# 的前向管道运算符 (|>)?

Is Haskell's bind operator (>>=) equivalent to F#'s forward pipe operator (|>)?

Haskell 的绑定运算符 (>>=) 的类型签名:

m a -> (a -> m b) -> m b

F# 的前向管道运算符 (|>) 的类型签名:

'a -> ('a -> 'b) -> 'b

它们看起来很相似。 考虑到 F# 的不纯性质, Haskell 中 |> 的等效运算符是 >>=?

例如:

Haskell:

getLine >>= putStrLn

F#:

stdin.ReadLine() |> stdout.Write

不是真的。如果您将 m 专门化为 IO,那么会有一些表面上的相似之处,所以也许 (>>=) @IO 有点像 F# 的 [=18] =],但一般来说,相似性不成立。

如果我们将 m 专门化为 Maybe,那么 >>= 就像 Option.bind,只是参数翻转了(这是有道理的,因为 >>=发音为“bind”)。

ghci> Just [1, 2, 3] >>= headMay
Just 1
ghci> Just [] >>= headMay
Nothing
ghci> Nothing >>= headMay
Nothing

如果我们将 m 专门化为 Either e,那么 >>= 会做类似于它对 Maybe 所做的事情,对 Left 值进行短路而不是 Nothing。这些示例有点类似于将 |> 用于引发异常的函数,但它们并不完全相同。

如果我们将 m 专门化为 Parser(例如来自 megaparsec 包),那么 >>= 会生成一个新的解析器 运行第一个解析器,然后使用它的结果来确定接下来 运行 哪个解析器。例如,这定义了一个解析器,该解析器生成一个解析器,该解析器解析两个数字或一个非数字后跟一个任意字符:

p :: Parser Char
p = anyChar >>= \c -> if isDigit c then digit else anyChar

这与 |> 有很大不同,因为我们没有 运行ning 任何东西,只是构建一个稍后将应用于值的结构(解析器),但是代码仍然在谈论最终将提供的价值(在 c 绑定中)。

如果我们将 m 专门化为 (->) r,那么 >>= 实现了一种隐式参数传递。例如,如果我们有一组函数都接受一个共同的参数:

f :: Key -> String
g :: String -> Key -> Char
h :: Char -> Key -> Bool

...然后我们可以使用>>=将它们组合在一起,将相同的第一个参数传递给所有它们:

ghci> :t f >>= g >>= h
f >>= g >>= h :: Key -> Bool

明显不同于|>,因为我们执行的是一种函数组合,而不是函数应用。

我可以继续,但是列出几十个示例可能并不比只列出几个更有帮助。要点是 >>= 不仅仅是为了对有效的事物进行排序,它是一个更通用的抽象,其中排序 IO 动作是一个特例。当然,IO 案例在实用上很有用,但它也可能是理论上最不有趣的案例,因为它有点神奇(IO 融入了 运行 时代)。 >>= 的这些其他用法一点也不神奇;它们完全是使用普通的纯 Haskell 代码定义的,但它们仍然非常有用,因此它们比 [=16] 更有助于理解 >>=Monad 的本质=]是。


最后说一句,Haskell 是否具有与 F# 的 |> 类似的功能。它叫做 &,它来自 Data.Function 模块。它与 F# 中的类型相同:

(&) :: a -> (a -> b) -> b

这个函数本身就很有用,但它与 monad 无关。

虽然 F# 不区分纯操作和非纯操作,但它确实有 monad 的概念。当您使用 computation expressions 时,这是最明显的。为了实现一个计算表达式,你必须实现monadic bind。在 F# 文档中,它必须具有类型 M<'T> * ('T -> M<'U>) -> M<'U>,尽管这是伪代码,因为像 M<'T> 这样的类型不是正确的 F# 语法。

F# 带有一些内置的 monad,例如 Async<'a>'a list'a seq。您还可以为 'a optionResult 简单地创建计算表达式,尽管我认为这些都不是内置的。

您可以仔细阅读各种计算表达式构建器的源代码,以确定如何为每个构建器实现单子绑定,但是 AJFarmar 是正确的,它们通常被称为 collect:

> List.collect;;
val it : (('a -> 'b list) -> 'a list -> 'b list)

> Array.collect;;
val it : (('a -> 'b []) -> 'a [] -> 'b [])

> Seq.collect;;
val it : (('a -> #seq<'c>) -> seq<'a> -> seq<'c>)

但并不总是如此。有时该操作称为 bind:

> Option.bind;;
val it : (('a -> 'b option) -> 'a option -> 'b option)

为了说明,请考虑这个将字符串解析为整数的小 F# 辅助函数:

open System

let tryParse s =
    match Int32.TryParse s with
    | true, i -> Some i
    | _ -> None

如果你有一个字符串,你可以使用正向管道:

> "42" |> tryParse;;
val it : int option = Some 42

另一方面,如果您的字符串已经在 option 值中,则您必须使用单子绑定:

> Some "42" |> Option.bind tryParse;;
val it : int option = Some 42

|>运算符也存在于Haskell中,但你必须导入Data.Function:

Prelude Data.Function> :t (&)
(&) :: a -> (a -> b) -> b