Haskell的绑定运算符(>>=)与F#的前向管道运算符(|>)等效吗?

7

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

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

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

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

他们看起来很相似。考虑到F#的不纯性质,Haskell中与|>等价的操作符是>>=
例如:
Haskell:
getLine >>= putStrLn

F#:

stdin.ReadLine() |> stdout.Write

3
No. |> 就像 $>>= 就像 *.collect,但更通用。 - AJF
6
潜在的选民们:这绝对不是基于观点的。通过考虑所涉及的语言类型和差异(参见现有的答案),可以客观地回答它。 - duplode
2个回答

10
不完全正确。如果将 m 专门用于 IO,那么可能存在一些表面上的相似之处,因此也许 (>>=) @IO 确实有点像 F# 的 |>,但总的来说,这种相似性并不成立。
如果我们将 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

这与|>大不相同,因为我们并没有运行任何东西,只是构建了一个结构(解析器),稍后将应用于一个值,但代码仍然谈论最终将提供的值(在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代码定义,但它们仍然非常有用,因此它们比IO更相关于理解>>=Monad的本质。


最后提一句,Haskell确实有一个类似F#的|>函数。它称为&,来自Data.Function模块。它具有与F#中相同的类型:

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

这个函数本身非常有用,但与单子无关。


  1. Key 这个东西行不通:a)我们需要 f :: Key -> String,b)Key 应该是另外两个参数的第二个(或者应该使用 (<*>))。2) (>>=)(实际上是 (=<<))是一种应用形式。有一个强大的塔:对于纯函数,使用 $/(&);对于 Functor,使用 <$>;对于 Applicative,使用 <*>/<**>;对于 Monad,使用 (=<<)/(>>=)
- HTNW
@HTNW 对于你的第一个观点:是的,你说得对,我不知道当时在想什么。我已经更新了示例,使其符合我最初设想的样子。至于你的第二个观点,我同意,但我认为这超出了本答案的范围。 - Alexis King

9
虽然F#不区分纯操作和非纯操作,但它确实有单子的概念。当您使用计算表达式时,这是最明显的。为了实现计算表达式,您必须实现monadic bind。在F#文档中,这必须具有类型M<'T> * ('T -> M<'U>) -> M<'U>,尽管这是伪代码,因为像M<'T>这样的类型不是正确的F#语法。
F#带有一些内置的单子,例如Async<'a>,'a list,'a seq。您还可以轻松创建'a option和Result的计算表达式,尽管我认为这些都没有内置。
您可以查看各种计算表达式生成器的源代码,以确定如何为每个实现单子绑定,但是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

另一方面,如果您的字符串已经在

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接