31

我刚刚开始使用 Scala,并且刚刚了解了如何使方法具有右关联性(与命令式面向对象语言中常见的更传统的左关联性相反)。

起初,当我cons在 Scala 中看到列表的示例代码时,我注意到每个示例的右侧总是有 List:

println(1 :: List(2, 3, 4))
newList = 42 :: originalList

然而,即使一遍又一遍地看到这个,我也没有多想,因为我(当时)不知道这::是一个方法List。我只是假设它是一个运算符(同样,在 Java 中的运算符意义上)并且关联性并不重要。List总是出现在示例代码右侧的事实似乎是巧合(我认为这可能只是“首选样式”)。

现在我知道了:它必须这样写,因为::它是右结合的。

我的问题是,能够定义右关联方法有什么意义?

纯粹是出于审美原因,还是在某些情况下,右联想实际上比左联想有某种好处?

从我(新手)的角度来看,我真的不明白如何

1 :: myList

myList :: 1

但这显然是一个微不足道的例子,我怀疑这是一个公平的比较。

4

3 回答 3

39

简短的回答是,右关联可以通过使程序员键入的内容与程序实际执行的内容一致来提高可读性。
所以,如果你输入 ' 1 :: 2 :: 3',你会得到一个 List(1, 2, 3),而不是以完全不同的顺序得到一个 List。
那是因为 ' 1 :: 2 :: 3 :: Nil' 实际上是

List[Int].3.prepend(2).prepend(1)

scala> 1 :: 2 :: 3:: Nil
res0: List[Int] = List(1, 2, 3)

两者都是:

  • 更具可读性
  • 更有效(O(1) 用于prepend,而 O(n) 用于假设append方法)

(提醒,摘自《Scala 编程》一书)
如果以运算符表示法使用方法,例如a * b,则在左操作数上调用该方法,如a.*(b)— 除非方法名称以冒号结尾。
如果方法名称以冒号结尾,则在右操作数上调用该方法。
因此,在 中1 :: twoThree::方法在 上调用twoThree,传入 1,如下所示:twoThree.::(1)

对于List,它起到了追加操作的作用(列表似乎是在'1'之后追加形成' 1 2 3',实际上它是1,它被追加到列表中)。
类 List 不提供真正的追加操作,因为追加到列表所需的时间随列表的大小线性增长,而在前面添加 :: 需要常量时间
myList :: 1会尝试将 myList 的全部内容添加到“1”,这将比在 myList 中添加 1 更长(如“ 1 :: myList”)

注意:不管运算符有什么关联性,它的操作数总是从左到右计算。
因此,如果 b 是一个不仅仅是对不可变值的简单引用的表达式,那么 a ::: b 更精确地被视为以下块:

{ val x = a; b.:::(x) }

在这个块中,a 仍然在 b 之前被评估,然后这个评估的结果作为操作数传递给 b 的 ::: 方法。


为什么要区分左关联方法和右关联方法?

这允许1 :: myList在实际将操作应用于右表达式的同时保持通常的左关联操作 (' ') 的外观,因为;

  • 它更有效。
  • 但使用逆关联顺序(' 1 :: myList' vs. ' myList.prepend(1)')更具可读性

据我所知,正如你所说,“语法糖”。
请注意,例如,在 的情况foldLeft下,它们可能走得有点远(与 ' /:' 右结合运算符等效)


为了包含您的一些评论,稍微改写一下:

如果你考虑一个“附加”函数,左关联,那么你会写“ oneTwo append 3 append 4 append 5”。
但是,如果要将 3、4 和 5 附加到 oneTwo(您会按照它的编写方式假设),它将是 O(N)。
如果是“追加”,则与 '::' 相同。但事实并非如此。它实际上是“前置”

这意味着 ' a :: b :: Nil' 是 ' List[].b.prepend(a)'

如果 '::' 在前面添加但仍保持左关联,则结果列表的顺序将错误。
您可能希望它返回 List(1, 2, 3, 4, 5),但它最终会返回 List(5, 4, 3, 1, 2),这可能出乎程序员的意料。
那是因为,您所做的将是按照左关联顺序:

(1,2).prepend(3).prepend(4).prepend(5) : (5,4,3,1,2)

因此,右关联性使代码与返回值的实际顺序相匹配。

于 2009-07-22T03:52:19.993 回答
3

能够定义右关联方法有什么意义?

我认为右关联方法的重点是给某人一个扩展语言的机会,这通常是运算符覆盖的重点。

运算符重载是一件有用的事情,因此 Scala 说:为什么不对任何符号组合开放它?相反,为什么要区分运算符和方法?现在,库实现者如何与内置类型交互Int?在 C++ 中,她会friend在全局范围内使用函数。如果我们希望所有类型都实现 operator怎么办::

右结合性提供了一种将::运算符添加到所有类型的简洁方法。当然,从技术上讲,::运算符是 List 类型的方法。但它也是内置 Int 和所有其他类型的虚构运算符,至少如果你可以忽略:: Nil最后的 。

我认为它反映了 Scala 在库中实现尽可能多的东西并使语言灵活地支持它们的理念。这使某人有机会提出 SuperList,可以称为:

1 :: 2 :: SuperNil

不幸的是,正确的关联性目前仅硬编码到最后的冒号,但我想它很容易记住。

于 2009-08-30T08:12:49.890 回答
3

执行列表折叠操作时,右关联性和左关联性起着重要作用。例如:

def concat[T](xs: List[T], ys: List[T]): List[T] = (xs foldRight ys)(_::_)

这工作得很好。但是您不能使用 foldLeft 操作执行相同的操作

def concat[T](xs: List[T], ys: List[T]): List[T] = (xs foldLeft ys)(_::_)

, 因为::是右结合的。

于 2013-10-28T18:07:56.050 回答