在 Elm 中,“<<”运算符是什么意思?

64
在下面的代码中,取自Elm表单示例,第122行的<<运算符是什么意思?
Field.field Field.defaultStyle (Signal.send updateChan << toUpdate) "" content

Elm 语法参考 中找不到它。

这是否意味着,当字段更改时,将其 content 发送到 updateChan 而是发送 toUpdateupdateChan


5
如果您熟悉 Haskell,可以将 << 视为 Haskell 中的 . - ZhekaKozlov
6个回答

138

<<是一个函数组合运算符,在Basics核心库中定义。所有来自Basics的函数都会被无条件地导入到Elm项目中。

Elm的类型系统

让我们回顾一下Elm类型系统的基础知识。

Elm是静态类型的。这意味着在Elm中,每个变量或函数都有一个类型,并且这种类型永远不会改变。在Elm中的类型示例包括:

  • Int
  • String
  • Maybe Bool
  • { name : String, age : Int }
  • Int -> Int
  • Int -> String -> Maybe Char
静态类型意味着编译器在编译期间确保所有函数和变量的类型都正确,因此您不会出现运行时类型错误。换句话说,您将永远不会有一个类型为String -> String的函数接收或返回Int,这样的代码甚至不会编译。
您还可以通过用类型变量替换具体类型(例如StringMaybe Int)来使您的函数具有多态性,类型变量是任意的小写字符串,例如a。许多Elm核心函数都是类型多态的,例如List.isEmpty的类型为List a -> Bool。它接受某种类型的List并返回一个Bool值。
如果你再次看到同一类型的变量,那么该类型变量的实例必须是相同类型。例如List.reverse的类型为List a -> List a。因此,如果你将List.reverse应用于整数列表(即具有类型List Int的内容),它将返回一个整数列表。这种函数不可能接收整数列表,然后返回字符串列表。编译器保证了这一点。

Elm中的所有函数默认都是柯里化的。这意味着,如果你有一个带有2个参数的函数,它会被转换成一个带有1个参数的函数,该函数返回一个带有1个参数的函数。这就是为什么Elm中的函数应用语法与Java、C++、C#、Python等其他语言中的函数应用不同的原因。当你可以写someFunction arg1 arg2时,没有理由去写someFunction(arg1, arg2)。为什么?因为实际上someFunction arg1 arg2等同于((someFunction arg1) arg2)

Currying使得部分应用成为可能。假设你想要部分应用List.memberList.member的类型是a -> List a -> Bool。我们可以将该类型解读为“List.member需要两个参数,一个是类型为a,另一个是类型为List a”。但我们也可以将该类型解读为“List.member需要一个类型为a的参数。它返回一个类型为List a -> Bool的函数”。因此,我们可以创建一个函数isOneMemberOf = List.member 1,它的类型为List Int -> Bool
这意味着函数类型注释中的->是右结合的。换句话说,a -> List a -> Boola -> (List a -> Bool)是相同的。

中缀和前缀符号

任何中缀运算符实际上都是一个普通函数。只不过当函数名仅由非字母数字符号(如$、<|、<<等)组成时,它被放置在两个参数之间,而不是在它们前面(像普通函数一样)。

但是您仍然可以将类似+的二元运算符放在两个参数前面,通过将其括在括号中,因此下面的两个函数应用程序是等价的:

2 + 3 -- returns 5
(+) 2 3 -- returns 5, just like the previous one

中缀运算符只是普通的函数。它们并没有什么特别之处。您可以像任何其他函数一样部分应用它们:

addTwo : Int -> Int
addTwo = (+) 2

addTwo 3 -- returns 5

函数组合

(<<) 是一个函数组合运算符,在核心库 Basics 中定义。从 Basics 导入的所有函数都是未经限定的,这意味着您不必编写 import Basics exposing (..),默认已经完成。

所以像其他运算符一样,(<<) 只是一个函数,就像任何其他函数一样。它的类型是什么?

(<<) : (b -> c) -> (a -> b) -> a -> c

由于 -> 是右结合的,因此这等同于:
(<<) : (b -> c) -> (a -> b) -> (a -> c)

换言之,(<<) 接收两个函数,类型分别为 b -> ca -> b,并返回一个类型为 a -> c 的函数。它将这两个函数合成为一个函数。那么它是如何工作的呢?为了简单起见,让我们看一个人为制造的例子。假设我们有两个简单函数:
addOne = (+) 1
multTwo = (*) 2

假设我们没有 (+),只有 addOne,那么如何创建一个加3而不是加1的函数?非常简单,我们将 addOne 组合在一起,共组合3次:
addThree : Int -> Int
addThree = addOne << addOne << addOne

如果我们想要创建一个函数,将数字加2,然后乘以4,该怎么办?
ourFunction : Int -> Int
ourFunction = multTwo << multTwo << addOne << addOne

(<<) 从右到左组合函数。但上面的例子很简单,因为所有类型都相同。我们如何找到列表中所有偶数立方体的总和?

isEven : Int -> Bool
isEven n = n % 2 == 0

cube : Int -> Int
cube n = n * n * n

ourFunction2 : List Int -> Int
ourFunction2 = List.sum << filter isEven << map cube

(>>)是相同的函数,但参数被翻转了,因此我们可以从左到右编写相同的组合:

ourFunction2 = map cube >> filter isEven >> List.sum

概述

当你看到像h << g << f这样的东西时,你就知道fgh是函数。当将这个构造h << g << f应用于一个值x时,你就知道:

  • Elm首先将f应用于x
  • 然后将上一步的结果应用于g
  • 最后将上一步的结果应用于h

因此,(negate << (*) 10 << sqrt) 25等于-50.0,因为你首先对25进行平方根运算得到5,然后将5乘以10得到50,再将50取反得到-50。

为什么使用<<而不是.

在 Elm 0.13 之前(参见announcement),函数组合运算符为 (.),其行为与当前的 (<<) 相同。 (<<) 是从 F# 语言中采用的(参见Github issue)。 Elm 0.13 还添加了 (>>),作为 flip (<<) 的等效形式,并将 (<|) 替换为函数应用运算符 ($),并将 (|>) 作为 flip (<|) 的等效形式。

中缀函数调用

你可能想知道如何将普通的字母数字函数名转换为中缀二进制运算符。在 Elm 0.18 之前,您可以使用反引号使函数成为中缀运算符,因此下面的2个表达式是等价的:

max 1 2 -- returns 2
1 `max` 2 -- returns 2

Elm 0.18 已删除此功能。你不能再在 Elm 中使用它,但像 HaskellPureScript 这样的语言仍然具有该功能。


14
我希望这个人能成为我的老师。我应该把这写进正式文档里,因为文档缺少一些对初学者在基于函数的机器学习语言方面很关键的信息。 - frostymarvelous

25

<< 是函数合成 - 返回一个函数。

函数合成可以创建一条计算管道,一系列的函数链。这个管道等待输入,当有输入时,第一个函数开始计算,将输出发送到下一个函数依次进行处理。

import Html

add x y =
    Debug.log "x" x + Debug.log "y" y

add9 =
    add 4 << add 5

main =
    Html.text <| toString <| add9 2

注意: 在上面的例子中,我使用了部分应用。这意味着我没有向函数提供所有参数,因此我得到了一个函数。

如果您在Web浏览器中运行上述示例并查看控制台输出,您将看到:

x: 5
y: 2
x: 4
y: 7

如果我们用数学运算符来表示它,那么它会看起来像这样:

4 + (5 + 2)
4 + 7

注意我们也可以使用前向版本>>

读取签名

查看该运算符的签名:

(<<) : (b -> c) -> (a -> b) -> a -> c

对于 << 运算符,第一个参数是函数 b -> c,第二个参数是函数 a -> b

(b -> c) << (a -> b)

但是也有第三个参数 a。因为 -> 是右关联的,所以

(<<) : (b -> c) -> (a -> b) -> a -> c

等价于:

(<<) : (b -> c) -> (a -> b) -> (a -> c)

因此,<< 返回函数 a -> c

结合性

在编程语言中,操作符的结合性(或固定性)是一种属性,它确定了没有括号时相同优先级的操作符如何分组;即每个操作符的计算顺序:

a = b = c 被解析为 a = (b = c)

中缀运算符

这里我使用 << 作为 中缀运算符,但我们也可以将其用作前缀运算符,并用括号括起来:(<<) (b -> c) (a -> b) 或者 (<|) (add 4) (add 5)

在 Elm 0.18 版本之前,可以将普通函数用作中缀运算符。

关于 <| 运算符的说明

<| 是一个函数应用 - 返回值

我们基本上使用它代替括号。

text (something ++ something)

可以写成

text <| something ++ something

因此,查看运算符的签名

(<|) : (a -> b) -> a -> b

我们可以看到在 <| 操作符中,第一个参数是函数 a -> b ,第二个参数是值 a:

(a -> b) <| a

它的返回值是 b

我们也可以使用函数应用的方式来得到相同的结果:<|

v1 = add 4 <| add 5 <| 4
v2 = (add 4 << add 5) 4
  • 这个操作符也有一个向前的版本 |>
  • 这里是该操作符的说明。
  • 为了清晰起见,请不要混用 <|<<

1
抱歉挑刺,但“惰性函数链”在这个上下文中适用吗?Elm不是严格求值的吗? - pdoherty926

11

这是函数合成。对于你的具体示例,它意味着

\x -> (Signal.send updateChan (toUpdate x))

在 Elm 中,它不是语法的一部分,而是标准库的一部分:Basics.<<


但是<<需要两个函数,如果fg都是函数,那么我可以看到f << g的意思是\x -> f (g x)。在我的例子中,toUpdate与谁组合,一些隐藏的恒等函数吗?由于updateChan不是一个函数,如果你写updateChan << toUpdate,这会导致错误吗? - Not an ID
我在您提供的资源中找不到这个片段。根据 send 和 Field 签名,我假设在那里使用了某种数据类型,因此 updateChan 是一个 Channel Data,而 toUpdate 是一个形式为 String -> Data 的函数。这给出了 (Signal.send updateChan) 具有类型 Data -> Message,而 (Signal.send updateChan) << toUpdate 具有类型 String -> Message - CheatEx

4

JavaScript开发人员的解释:

--elm

(a << b) x

将会类似。
//javasript

a(b(x))

<<>> 被称为函数组合


4

我的第二次尝试:D

<< vs <|

<<<|的区别在于<<用于组合函数,而<|用于省略括号。

为什么会这样工作

让我们看一下在这里找到的类型注释:

<< : (b -> c) -> (a -> b) -> a -> c

这个定义告诉我们,当你将两个函数传递给函数<<时,你会得到一个函数a -> c

示例:demo

hi a =
    a + 2
hello a =
    a * 2
bye =
    hello << hi
c =
    bye 3

c 返回值 10

阅读更多:

  • 中缀运算符 - 函数左侧的第一个参数,
  • 部分应用 - 当您向期望两个参数的函数传递一个参数时,您会得到一个期望一个参数的函数。

0
在这个回复中,我展示了<|<<之间的区别。
> addOne a = a + 1
<function> : number -> number

带有<|的代码示例:

> addThree a = addOne <| addOne <| addOne a
<function> : number -> number
> addThree 4
7 : number

addThree 基于函数组合 << 的备用版本:

> addThreeComposition = addOne << addOne << addOne
<function> : number -> number
> addThreeComposition 4
7 : number

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