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

发布于 2024-07-27 04:44:35 字数 733 浏览 3 评论 0原文

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

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

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

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

现在我更清楚了:它必须这样写,因为 :: 是右关联的。

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

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

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

1 :: myList

什么比这更好的了,

myList :: 1

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

I've just started playing around with Scala, and I just learned about how methods can be made right-associative (as opposed to the more traditional left-associativity common in imperative object-oriented languages).

At first, when I saw example code to cons a list in Scala, I had noticed that every example always had the List on the right-hand side:

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

However, even after seeing this over and over again, I didn't think twice about it, because I didn't know (at the time) that :: is a method on List. I just assumed it was an operator (again, in the sense of operator in Java) and that associativity didn't matter. The fact that the List always appeared on the right-hand side in example code just seemed coincidental (I thought it was maybe just the "preferred style").

Now I know better: it has to be written that way because :: is right-associative.

My question is, what is the point of being able to define right-associative methods?

Is it purely for aesthetic reasons, or can right-associativity actually have some kind of benefit over left-associativity in certain situations?

From my (novice) point-of-view, I don't really see how

1 :: myList

is any better than

myList :: 1

but that's obviously such a trivial example that I doubt it's a fair comparison.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

月下客 2024-08-03 04:44:35

简而言之,右关联性可以通过使程序员键入的内容与程序实际执行的内容一致来提高可读性。
因此,如果您输入“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),而不是 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”,这比将 1 添加到 myList 更长(如“1 :: myList”) )

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

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

在该块中,a 仍然先于 b 进行求值,然后是此求值的结果
作为操作数传递给 b 的 ::: 方法。


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

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

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

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


要包括您的一些评论,请稍微改写:

if如果您考虑一个左关联的“追加”函数,那么您将编写“oneTwoappend 3append4append5”。
但是,如果要将 3、4 和 5 附加到 oneTwo(您可以按照其编写方式进行假设),则复杂度为 O(N)。
如果用于“追加”,则与“::”相同。 但事实并非如此。 它实际上用于“prepend”,

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

If ':: ' 放在前面但仍保持左关联,那么结果列表的顺序将是错误的。
您希望它返回 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)

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

The short answer is that right-associativity can improve readability by making what the programmer type consistent with what the program actually does.
So, if you type '1 :: 2 :: 3', you get a List(1, 2, 3) back, instead of getting a List back in a completely different order.
That would be because '1 :: 2 :: 3 :: Nil' is actually

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

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

which is both:

  • more readable
  • more efficient (O(1) for prepend, vs. O(n) for an hypothetic append method)

(Reminder, extract from the book Programming in Scala)
If a method is used in operator notation, such as a * b, the method is invoked on the left operand, as in a.*(b) — unless the method name ends in a colon.
If the method name ends in a colon, the method is invoked on the right operand.
Therefore, in 1 :: twoThree, the :: method is invoked on twoThree, passing in 1, like this: twoThree.::(1).

For List, it plays the role of an append operation (the list seems to be appended after the '1' to form '1 2 3', where in fact it is 1 which is prepended to the list).
Class List does not offer a true append operation, because the time it takes to append to a list grows linearly with the size of the list, whereas prepending with :: takes constant time.
myList :: 1 would try to prepend the entire content of myList to '1', which would be longer than prepending 1 to myList (as in '1 :: myList')

Note: No matter what associativity an operator has, however, its operands are
always evaluated left to right.
So if b is an expression that is not just a simple reference to an immutable value, then a ::: b is more precisely treated as the following block:

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

In this block a is still evaluated before b, and then the result of this evaluation
is passed as an operand to b’s ::: method.


why make the distinction between left-associative and right-associative methods at all?

That allows to keep the appearance of a usual left-associative operation ('1 :: myList') while actually applying the operation on the right expression because;

  • it is more efficient.
  • but it is more readable with an inverse associative order ('1 :: myList' vs. 'myList.prepend(1)')

So as you say, "syntactic sugar", as far as I know.
Note, in the case of foldLeft, for instance, they might have gone a little to far (with the '/:' right-associative operator equivalent)


To include some of your comments, slightly rephrased:

if you consider an 'append' function, left-associative, then you would write 'oneTwo append 3 append 4 append 5'.
However, if it were to append 3, 4, and 5 to oneTwo (which you would assume by the way it's written), it would be O(N).
Same with '::' if it was for "append". But it is not. It is actually for "prepend"

That means 'a :: b :: Nil' is for 'List[].b.prepend(a)'

If '::' were to prepend and yet remain left-associative, then the resulting list would be in the wrong order.
You would expect it to return List(1, 2, 3, 4, 5), but it would end up returning List(5, 4, 3, 1, 2), which might be unexpected to the programmer.
That is because, what you have done would have been, in the left-associative order:

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

So, the right-associativity makes the code match up with the actual order of the return value.

春夜浅 2024-08-03 04:44:35

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

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

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

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

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

1 :: 2 :: SuperNil

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

What is the point of being able to define right-associative methods?

I think the point of right-associative method is to give someone an opportunity to extend the language, which is the point of operator overriding in general.

Operator overloading is a useful thing, so Scala said: Why not open it up to any combination of symbols? Rather, why make distinction between the operators and methods? Now, how does a library implementor interact with built-in types like Int? In C++, she would have used friend function in the global scope. What if we want all types to implement the operator ::?

The right-associativity gives a clean way to add :: operator to all types. Of course, technically the :: operator is a method of List type. But it's also a make-believe operator for built-in Int and all of the other types, at least if you could ignore the :: Nil at the end.

I think it reflects Scala's philosophy of implementing as much things in the library and making the language flexible to support them. This allows an opportunity for someone to come up with SuperList, which could be called as:

1 :: 2 :: SuperNil

It's somewhat unfortunate that the right associativity is currently hard-coded only to the colon at the end, but I guess it makes it easy enough to remember.

妄想挽回 2024-08-03 04:44:35

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

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)(_::_)

,因为 :: 是右关联的。

Right-associativity and Left-associativity plays an important role when you are performing List fold operations. For eg:

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

This works perfectly fine. But you cannot perform the same using foldLeft operation

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

,because :: is right-associative.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文