Scala 中的右关联方法有啥好处?
Posted
技术标签:
【中文标题】Scala 中的右关联方法有啥好处?【英文标题】:What good are right-associative methods in Scala?Scala 中的右关联方法有什么好处? 【发布时间】:2010-11-12 20:55:20 【问题描述】:我刚刚开始使用 Scala,并且刚刚了解了如何使方法右关联(与更传统的左关联相反在命令式面向对象语言中很常见)。
起初,当我在 Scala 中看到 cons
列表的示例代码时,我注意到每个示例的右侧总是有列表:
println(1 :: List(2, 3, 4))
newList = 42 :: originalList
但是,即使一遍又一遍地看到这个,我也没有多想,因为我(当时)不知道::
是List
上的一个方法。我只是假设它是一个运算符(同样,在 Java 中的运算符意义上)并且关联性并不重要。 List
总是出现在示例代码的右侧似乎是巧合(我认为这可能只是“首选样式”)。
现在我知道了:它必须这样写,因为::
是右结合的。
我的问题是,能够定义右关联方法有什么意义?
纯粹是出于审美原因,还是在某些情况下,右关联实际上比左关联有某种好处?
从我(新手)的角度来看,我真的不明白如何
1 :: myList
比
好myList :: 1
但这显然是一个微不足道的例子,我怀疑这是一个公平的比较。
【问题讨论】:
刚刚在我的回答中包含了您的 cmets,并附有一些示例来说明它们。 据我所知,这是为了让 Lisp 用户有宾至如归的感觉,纯粹是为了允许 :: 列表追加操作。 @skaffman 不,不是。阅读答案。 【参考方案1】:简短的回答是,右关联可以通过使程序员键入的内容与程序实际执行的内容保持一致来提高可读性。
因此,如果您输入“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)
两者兼有:
更具可读性 效率更高(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,即前置到列表中)。
类 List 不提供真正的追加操作,因为追加到列表所需的时间随着列表的大小线性增长,而 用 :: 前置需要固定时间。@987654335 @ 会尝试将 myList 的全部内容添加到“1”,这将比将 1 添加到 myList 更长(如“1 :: myList
”)
注意:不管一个运算符有什么结合性,但是,它的操作数是 总是从左到右评估。 因此,如果 b 是一个不仅仅是对不可变值的简单引用的表达式,那么 a ::: b 更精确地被视为以下块:
val x = a; b.:::(x)
在这个块中a仍然在b之前被评估,然后是这个评估的结果 作为操作数传递给 b 的 ::: 方法。
为什么要区分左关联方法和右关联方法?
这允许在将操作实际应用于右表达式的同时保持通常的左关联操作 ('1 :: myList
') 的外观,因为;
1 :: myList
' vs. 'myList.prepend(1)
')更具可读性
据我所知,正如您所说,“语法糖”。
请注意,以foldLeft
为例,它们可能具有gone a little to far(与'/:
' 右关联运算符等效)
为了包括你的一些 cmets,稍微改写一下:
如果您考虑左关联的“附加”函数,那么您将编写“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)
因此,右关联性使代码与返回值的实际顺序相匹配。
【讨论】:
是的,我知道以冒号 (:) 结尾的方法应用于右侧,并且我已经看到了添加到 List 的解释是 O(1) 而不是O(N) 用于追加。我的问题更多是为什么要区分左关联方法和右关联方法?我试图找出在语言中设计对右关联方法的支持的基本原理。我的猜测是它纯粹是为了语法糖,但我想知道这个设计选择是否有更多的想法,而不是仅仅看代码。 好的。我想我明白你在说什么。如果 '::' 是左关联的,那么你会写成 'oneTwo :: 3 :: 4 :: 5"。但是,如果要将 3、4 和 5 附加到 oneTwo(顺便说一下,你会假设它是写的),它将是 O(N)。但是,如果 '::' 被添加但仍然保持左关联,那么结果列表的顺序将是错误的。你会期望它返回 List(1, 2 , 3, 4, 5),但最终会返回 List(5, 4, 3, 1, 2),这对程序员来说可能是出乎意料的。因此,右关联性使代码与实际顺序匹配返回值 伙计,真的很难解释我在这些小评论框中的意思;)简短的回答是,似乎右关联可以通过使程序员键入的内容与程序的内容保持一致来提高可读性实际上确实如此。因此,如果您键入 '1 :: 2 :: 3',您将得到一个 List(1, 2, 3),而不是以完全不同的顺序返回一个 List。 @Mike:是的,我在您编写 cmets 时添加到我的答案中;) 我想补充一点: List(a, b, c).::(i) 实际上以 (i, rest) 形式创建一个列表,其中 rest 指向 List(a, b, C)。这就是为什么 prepend 是 O(1)。【参考方案2】:能够定义右关联方法有什么意义?
我认为右关联方法的重点是给某人一个扩展语言的机会,这通常是运算符覆盖的重点。
运算符重载是一件有用的事情,因此 Scala 说:为什么不对任何符号组合开放它?相反,为什么要区分运算符和方法?现在,库实现者如何与像Int
这样的内置类型交互?在 C++ 中,她会在全局范围内使用 friend
函数。如果我们希望 所有 类型都实现运算符 ::
怎么办?
右关联性提供了一种将::
运算符添加到所有类型的简洁方法。当然,从技术上讲,::
运算符是 List 类型的方法。但它也是内置 Int 和其他类型的 all 的虚构运算符,至少如果你可以忽略最后的 :: Nil
。
我认为它反映了 Scala 的理念,即在库中实现尽可能多的东西并让语言灵活地支持它们。这让某人有机会提出 SuperList,可以称为:
1 :: 2 :: SuperNil
有点遗憾的是,正确的关联性目前仅硬编码到最后的冒号,但我想它很容易记住。
【讨论】:
【参考方案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)(_::_)
,因为::
是右关联的。
【讨论】:
以上是关于Scala 中的右关联方法有啥好处?的主要内容,如果未能解决你的问题,请参考以下文章