Haskell中的组合(.)与F#中的管道向前运算符(|>)

110
在F#中,使用管道前进运算符|>是非常常见的。然而,在Haskell中,我只看到过函数组合(.)被使用。我理解它们是相关的,但是在Haskell中为什么不使用管道前进运算符,还是有其他原因吗?

4
lihanseys answer 表示 & 是 Haskell 中的 |>。这个信息藏在这个帖子深处,让我花费了几天时间才找到。我经常使用它,因为你自然地从左向右阅读代码。 - itmuckel
10个回答

92
在F#中,(|>)很重要,因为它支持从左到右的类型检查。例如:
List.map (fun x -> x.Value) xs

一般不会进行类型检查,因为即使已知xs的类型,当类型检查器看到lambda的参数x时,它并不知道如何解析x.Value,所以无法确定其类型。

相比之下,

xs |> List.map (fun x -> x.Value)

这将起作用,因为xs的类型会导致x的类型被识别。

从左到右的类型检查是必需的,因为涉及到像x.Value这样的构造中的名称解析。Simon Peyton Jones已经提出了一个类似于Haskell的名称解析的建议,但他建议使用本地约束来跟踪类型是否支持特定操作。因此,在第一个示例中,需要x具有Value属性的要求将一直保留,直到看到xs并且可以解决该要求。不过,这确实使类型系统变得更加复杂。


1
有趣的是,Haskell 中有一个类似于点号 (.) 的 (<|) 运算符,方向相同,都是从右到左。但是它的类型解析会如何工作呢? - The_Ghost
7
(<|) 实际上类似于 Haskell 的 ($)。从左到右的类型检查只需要用于解决 .Value 等问题,因此在其他情况下 (<|) 也可以正常工作,或者如果您使用显式类型注释。 - GS - Apologise to Monica

64

我有些推测...

文化: 我认为在F# "文化"中,|> 是一个重要的运算符,类似地,在Haskell中使用 . 。F# 有一个函数组合运算符<<,但我认为 F# 社区不像 Haskell 社区那样经常使用点函式风格

语言差异: 我不了解这两种语言足够多,无法比较它们之间的差异,也许用于一般化let绑定的规则是足够不同以至于会影响到这个问题。例如,我知道在 F# 中有时写

let f = exp

代码无法编译,需要进行显式 eta 转换:

let f x = (exp) x   // or x |> exp

为了使其编译,这也使人们远离点式/组合风格,转向管道风格。此外,F#类型推断有时要求使用管道,以便已知类型出现在左侧(请参见此处)。

(个人认为,点式风格难以阅读,但我认为每一种新的/不同的东西都在你习惯之前难以阅读。)

我认为两种风格在任何语言中都有潜力可行,历史/文化/偶然性可能定义了为什么每个社区都定居在不同的“吸引点”上。


7
我同意文化差异。传统的Haskell使用.$,所以人们继续使用它们。 - Amok
9
Point free(无参考)有时比pointful(有参考)更易读,有时则不然。我通常在函数参数中使用它,如map和filter,以避免使用lambda使事情变得混乱。有时我也会在顶层函数中使用它,但较少且仅在某些简单明了的情况下使用。 - Paul Johnson
2
我认为这方面并没有太多文化,因为就 F# 而言,事实上并没有太多选择(正如你和 Ganesh 提到的那样)。所以我认为在 Haskell 中两者都是可行的,但是 F# 明显更适合使用管道运算符。 - Kurt Schelfthout
2
是的,阅读从左到右的文化 :) 即使数学也应该这样教... - nicolas
1
@nicolas 左到右只是一种任意的选择。你可以适应从右到左。 - Martin Capodici
显示剩余2条评论

47

有更多来自以Haskell为主的推测......

($)(|>) 的翻转版本,并且在无法编写点免代码时使用相当普遍。因此,(|>) 在Haskell中没有被使用的主要原因是它的位置已经被 ($)占据了。

另外,从一些F#的经验来看,我认为 (|>) 在F#代码中如此流行,是因为它类似于面向对象的结构Subject.Verb(Object)。由于F#旨在实现平滑的函数式/面向对象集成,Subject |> Verb Object 对于新的函数式编程人员来说是一个相当顺畅的过渡。

个人而言,我也喜欢从左到右思考,所以我在Haskell中使用 (|>),但我认为其他人并不会那么做。


嗨,Nathan,Haskell 平台上是否有预定义的“flip ($)”?名称“(>|)”已经与另一含义在 Data.Sequence 中定义。如果还未定义,则应该如何称呼它?我正在考虑使用“($>) = flip ($)”。 - mattbh
2
@mattbh:我在Hoogle上找不到这个。我不知道Data.Sequence.|>,但$>看起来是避免冲突的合理选择。说实话,只有那么多好看的运算符,所以我会同时使用|>来处理冲突的情况。(另外,我会想把Data.Sequence.|>重命名为snoc)。 - Nathan Shively-Sanders
($)只是重新定义了解析结合性。它与前向组合不同-您必须在范围内绑定名称。请自行尝试:http://pastebin.com/uEc2k612 - gatoatigrado
2
($)(|>)是应用而不是组合。它们之间有关联(正如问题所指出的),但它们并不相同(对于函数,您的fc(Control.Arrow.>>>))。 - Nathan Shively-Sanders
16
实际上,F#中的 |> 符号让我想起了UNIX中的 | 更甚于其他任何东西。 - Kevin Cantu
6
F# 中 |> 的另一个好处是它在 Visual Studio 的 IntelliSense 中具有良好的属性。输入 |>,您将获得可以应用于左侧值的函数列表,类似于在对象后键入 . 时发生的情况。 - Kristopher Johnson

34

我觉得我们把事情搞混了。Haskell中的(.)等同于F#中的(>>)。不要与F#中的(|>)混淆,后者只是反向函数应用,类似于Haskell中的($) - 反转:

let (>>) f g x = g (f x)
let (|>) x f = f x

我相信Haskell程序员经常使用 $。也许不像F#程序员经常使用 |> 那样频繁。另一方面,有些F#程序员过度使用 >>,甚至到了荒唐的程度:http://blogs.msdn.com/b/ashleyf/archive/2011/04/21/programming-is-pointless.aspx


3
正如你所说,这就像是 Haskell 的 $ 运算符的反向操作。你也可以很容易地将其定义为:a |> b = flip ($), 这等同于 F# 的管道操作。例如,你可以使用 [1..10] |> map f 的方式来使用它。请注意不要改变原文意思,并尽可能使翻译通俗易懂。 - vis
8
我认为(.) 和 (<<) 的意思相同,而 (>>) 则是反向组合。即 ( >> ) : ('T1 -> 'T2) -> ('T2 -> 'T3) -> 'T1 -> 'T3( << ) : ('T2 -> 'T3) -> ('T1 -> 'T2) -> 'T1 -> 'T3 - Dobes Vandermeer
-1 对于“使用>>到荒谬的程度”(好吧,我没有,但你懂我的意思)。F#中的F代表“函数式”,因此函数组合是合法的。 - Florian F
4
当然,我同意函数合成是可行的。在“一些F#小伙伴”中,我指的就是我自己!这是我的博客。 :P - AshleyF
我认为 . 不等同于 >>。我不知道 F# 是否有 <<,但那将是等效的(就像 Elm 中一样)。 - Matt Joiner

32
如果您想在Haskell中使用F#的|>,则可以在Data.Function中使用&运算符(自base 4.8.0.0以来)。

2
Haskell 选择 & 而不是 |> 的原因是什么?我觉得 |> 更直观,而且它也让我想起了 Unix 管道操作符。 - yeshengm
2
@yeshengm 我觉得 & 很直观。只需将 & 发音为 "and",代码几乎可以用英语正确地读出来。例如:5 & factorial & show 可以朗读为 "take 5 and then take factorial of it and then apply show to it"。 - Marcel Besixdouze

17

Haskell中的左到右组合

有些人在Haskell中也使用左到右(消息传递)风格。例如,在Hackage上可以看到mps库的示例:

euler_1 = ( [3,6..999] ++ [5,10..999] ).unique.sum

我认为在某些情况下这种样式看起来很好,但是它更难以阅读(需要了解库及其所有运算符,重新定义的(.)也很困扰)。

Control.Category中还有从左到右和从右到左的组合运算符,这是base包的一部分。 分别比较>>><<<

ghci> :m + Control.Category
ghci> let f = (+2) ; g = (*3) in map ($1) [f >>> g, f <<< g]
[9,5]

有时候有充分的理由偏爱从左到右的组合:计算顺序遵循阅读顺序。


不错,所以 (>>>) 在很大程度上等同于 (|>)? - Khanzor
@Khanzor 不完全是这样。(|>) 应用于参数,(>>>) 大多是函数组合(或类似的东西)。然后我想可能有一些优先级差异(没有检查过)。 - sastanin

17

我认为在Haskell中,F#的管道正向运算符(|>)应该被使用,而不是(&)

// pipe operator example in haskell

factorial :: (Eq a, Num a) =>  a -> a
factorial x =
  case x of
    1 -> 1
    _ -> x * factorial (x-1)

// terminal
ghic >> 5 & factorial & show

如果你不喜欢(&)运算符,你可以像F#或Elixir一样自定义它:

(|>) :: a -> (a -> b) -> b
(|>) x f = f x
infixl 1 |>
ghci>> 5 |> factorial |> show

为什么要用 infixl 1 |>?请参见Data-Function (&)的文档。

  

infixl = 带左结合性的中缀运算符

     

infixr = 带右结合性的中缀运算符


(.)

(.) 表示函数组合。在数学上它表示(f.g)(x) = f(g(x))

foo = negate . (*3)
// ouput -3
ghci>> foo 1
// ouput -15
ghci>> foo 5

它等于

// (1)
foo x = negate (x * 3) 
或者
// (2)
foo x = negate $ x * 3 

($) 运算符在Data-Function ($)中也有定义。

(.) 用于创建高阶函数或JavaScript中的闭包。请参见示例:


// (1) use lamda expression to create a Hight Order Function
ghci> map (\x -> negate (abs x)) [5,-3,-6,7,-3,2,-19,24]  
[-5,-3,-6,-7,-3,-2,-19,-24]


// (2) use . operator to create a Hight Order Function
ghci> map (negate . abs) [5,-3,-6,7,-3,2,-19,24]  
[-5,-3,-6,-7,-3,-2,-19,-24]

哇,更简洁的代码更好。


比较 |>.

ghci> 5 |> factorial |> show

// equals

ghci> (show . factorial) 5 

// equals

ghci> show . factorial $ 5 

这是左到右和右到左之间的不同。⊙﹏⊙|||

人性化

|>&. 更好。

因为

ghci> sum (replicate 5 (max 6.7 8.9))

// equals

ghci> 8.9 & max 6.7 & replicate 5 & sum

// equals

ghci> 8.9 |> max 6.7 |> replicate 5 |> sum

// equals

ghci> (sum . replicate 5 . max 6.7) 8.9

// equals

ghci> sum . replicate 5 . max 6.7 $ 8.9

如何在面向对象语言中进行函数式编程?

请访问http://reactivex.io/

它支持以下语言:

  • Java: RxJava
  • JavaScript: RxJS
  • C#: Rx.NET
  • C#(Unity): UniRx
  • Scala: RxScala
  • Clojure: RxClojure
  • C++: RxCpp
  • Lua: RxLua
  • Ruby: Rx.rb
  • Python: RxPY
  • Go: RxGo
  • Groovy: RxGroovy
  • JRuby: RxJRuby
  • Kotlin: RxKotlin
  • Swift: RxSwift
  • PHP: RxPHP
  • Elixir: reaxive
  • Dart: RxDart

我唯一看到的替换 FSharp 中 |> 为 Haskell 中的 & 的问题是,你(据说)不能在行首使用它。 - Matthias Schuster

17

我看到过在 flip (.) 中使用 >>>,并且我经常自己使用它,特别是对于最好从左到右理解的长链。

>>> 实际上来自 Control.Arrow,并且可以作用于不只是函数。


>>>Control.Category 中被定义。 - Steven Shaw

15
除了风格和文化之外,这归结为优化语言设计以用于纯或杂质代码。
F#中的 | > 运算符很常见,主要是因为它有助于隐藏与主要杂质代码出现的两个限制:
• 不带结构子类型的从左到右类型推断。
• 值约束。
请注意,前一个限制在OCaml中不存在,因为子类型是结构性的而不是标称性的,所以可以通过统一进行结构性类型的细化,随着类型推断的进行。
Haskell采取了不同的权衡,选择专注于主导纯代码,这些限制可以消除。

1
这是我尝试 Haskell 的第一天(之前学过 Rust 和 F#),我成功地定义了 F# 的 |> 运算符:
(|>) :: a -> (a -> b) -> b
(|>) x f = f x
infixl 0 |>

而且它似乎能够工作:

factorial x =
  case x of
    1 -> 1
    _ -> x * factorial (x-1)

main =     
    5 |> factorial |> print

我打赌一个Haskell专家能给你一个更好的解决方案。


2
你可以定义中缀运算符 infix :)x |> f = f x - Jamie

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