Scala中的右关联方法有什么用?

时间:2009-07-22 03:26:38

标签: scala operators operator-precedence

我刚刚开始使用Scala,我刚学会了如何制作方法右关联(而不是更传统的左关联在命令式面向对象语言中很常见。)

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

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

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

现在我知道的更好:必须以这种方式编写,因为::是正确关联的。

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

这纯粹是出于美学原因,还是在某些情况下,权利关联实际上会比左关联性有某些好处?

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

1 :: myList

更好
myList :: 1

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

3 个答案:

答案 0 :(得分:36)

简短的回答是,权利关联可以通过使程序员类型与程序实际执行的内容一致来提高可读性。
因此,如果您输入“1 :: 2 :: 3”,则会返回一个列表(1,2,3),而不是以完全不同的顺序返回列表。
那是因为'1 :: 2 :: 3 :: Nil'实际上是

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

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

两者都是:

  • 更具可读性
  • 效率更高(prepend为O(1),假设append方法为O(n)

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

对于List,它扮演一个追加操作的角色(列表似乎附加在'1'之后形成'1 2 3',其中实际上它是1 prepended 到列表中。) 类列表不提供真正的追加操作,因为附加到列表所需的时间与列表的大小呈线性增长,而前置为::需要恒定时间
myList :: 1会尝试将myList的整个内容添加到'1',这比将1添加到myList更长(如'1 :: myList')

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

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

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


  

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

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

  • 效率更高。
  • 但使​​用反关联顺序('1 :: myList'与'myList.prepend(1)')更具可读性

据我所知,就像你所说的那样,“语法糖” 请注意,例如,在foldLeft的情况下,它们可能有gone a little to far(与/:'右关联运算符等效'


要包含一些评论,请稍微改写一下:

如果你考虑一个'附加'功能,左联想,那么你会写'oneTwo append 3 append 4 append 5'。
但是,如果要将3,4和5附加到oneTwo(您可以通过它的编写方式来假设),那么它将是O(N)。
如果是“追加”,则与'::'相同。但事实并非如此。它实际上是“prepend”

这意味着“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)

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

答案 1 :(得分:3)

  

能够定义右关联方法的重点是什么?

我认为权利关联方法的目的是让某人有机会扩展语言,这一般是运算符覆盖的重点。

运算符重载是一个有用的东西,因此Scala说:为什么不将它打开到任何符号组合?相反,为什么要区分运算符和方法?现在,库实现者如何与Int等内置类型进行交互?在C ++中,她会在全局范围内使用friend函数。如果我们希望所有类型实现运算符::

,该怎么办?

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

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

1 :: 2 :: SuperNil

有点不幸的是,正确的关联性目前只对结尾的冒号进行了硬编码,但我想这很容易记住。

答案 2 :(得分: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)(_::_)

,因为::是右关联的。