Что хорошего в правоассоциативных методах в Scala?

Я только начал экспериментировать со Scala и узнал, как можно сделать методы правоассоциативными (в отличие от более традиционной левоассоциативности, характерной для императивных методов). объектно-ориентированные языки).

Сначала, когда я увидел пример кода для cons списка в Scala, я заметил, что в каждом примере список всегда был справа:

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

Однако, даже увидев это снова и снова, я не думал об этом дважды, потому что я не знал (в то время), что :: является методом на List. Я просто предположил, что это оператор (опять же, в смысле оператора в Java), и что ассоциативность не имеет значения. Тот факт, что List всегда появлялся справа в коде примера, казался случайным (я думал, что это, возможно, просто «предпочтительный стиль»).

Теперь я знаю лучше: так надо писать, потому что :: правоассоциативно.

Мой вопрос: в чем смысл определения правоассоциативных методов?

Это чисто из эстетических соображений, или правая ассоциативность действительно может иметь какое-то преимущество перед левой ассоциативностью в определенных ситуациях?

С моей (новичковой) точки зрения я действительно не понимаю, как

1 :: myList

лучше, чем

myList :: 1

но это, очевидно, настолько тривиальный пример, что я сомневаюсь, что это справедливое сравнение.


person Mike Spross    schedule 22.07.2009    source источник
comment
Просто включил ваши комментарии в свой ответ с некоторыми примерами, иллюстрирующими их.   -  person VonC    schedule 22.07.2009
comment
Насколько я могу судить, это сделано для того, чтобы пользователи Лиспа чувствовали себя как дома, просто чтобы разрешить операцию добавления ::list.   -  person skaffman    schedule 22.07.2009
comment
@skaffman нет, это не так. прочитай ответ.   -  person Daniel C. Sobral    schedule 22.07.2009


Ответы (3)


Короткий ответ заключается в том, что правая ассоциативность может улучшить читаемость, делая то, что набирает программист, согласующимся с тем, что на самом деле делает программа. вместо того, чтобы возвращать список в совершенно другом порядке.
Это было бы потому, что '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), если только имя метода не заканчивается двоеточием.
Если имя метода заканчивается двоеточием, метод вызывается для правого операнда.< br> Следовательно, в 1 :: twoThree метод :: вызывается для twoThree, передавая 1, вот так: twoThree.::(1).

Для списка это играет роль операции добавления (кажется, что список добавляется после '1', образуя '1 2 3', где на самом деле это 1, который предваряется списком).
Class List не предлагает настоящую операцию добавления, потому что время, необходимое для добавления к списку, растет линейно с размером списка, тогда как добавление с помощью :: занимает постоянное время.
myList :: 1 попытается добавить все содержимое myList к '1', что будет длиннее, чем добавление 1 к myList (как в '1 :: myList')

Примечание. Независимо от того, какой ассоциативностью обладает оператор, его операнды всегда вычисляются слева направо.
Таким образом, если b — это выражение, которое не является простой ссылкой на неизменяемое значение, то a ::: b более точно обрабатывается как следующий блок:

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

В этом блоке a по-прежнему оценивается перед b, а затем результат этой оценки передается в качестве операнда в метод b :::.


зачем вообще проводить различие между левоассоциативными и правоассоциативными методами?

Это позволяет сохранить видимость обычной левоассоциативной операции ('1 :: myList') при фактическом применении операции над правым выражением, потому что;

  • это более эффективно.
  • но он более удобочитаем с обратным ассоциативным порядком ('1 :: myList' против 'myList.prepend(1)')

Насколько я знаю, как вы говорите, "синтаксический сахар".
Обратите внимание, что в случае foldLeft, например, они могут иметь зашел слишком далеко (с эквивалентом правоассоциативного оператора '/:')


Чтобы включить некоторые из ваших комментариев, слегка перефразированных:

если вы рассматриваете функцию «добавления», левоассоциативную, то вы бы написали «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)

Итак, правая ассоциативность заставляет код соответствовать фактическому порядку возвращаемого значения.

person VonC    schedule 22.07.2009
comment
Да, я знаю, что методы, оканчивающиеся двоеточием (:), применяются к правой стороне, и я видел объяснение, что добавление к списку — это O(1), а не O(N) для добавления. Мой вопрос больше в том, зачем вообще проводить различие между лево-ассоциативными и право-ассоциативными методами? Я пытаюсь понять причину разработки поддержки правоассоциативных методов в языке. Я предполагаю, что это чисто для синтаксического сахара, но мне интересно, было ли больше внимания уделено этому выбору дизайна, чем просто внешний вид кода. - person Mike Spross; 22.07.2009
comment
В ПОРЯДКЕ. Кажется, я понимаю, к чему ты клонишь. Если бы '::' был левоассоциативным, то вы бы написали 'oneTwo :: 3 :: 4 :: 5. Однако, если бы он добавлял 3, 4 и 5 к oneTwo (что вы бы предположили, судя по тому, как он написано), это будет O(N). Однако, если бы '::' был добавлен в начало, но оставался бы левоассоциативным, то результирующий список был бы в неправильном порядке. Вы ожидаете, что он вернет List(1, 2, 3, 4, 5), но в итоге он вернет List(5, 4, 3, 1, 2), что может оказаться неожиданным для программиста. Таким образом, правильная ассоциативность заставляет код соответствовать фактическому порядку возвращаемого значения. - person Mike Spross; 22.07.2009
comment
Чувак, очень трудно объяснить, что я имею в виду в этих крошечных полях для комментариев ;) Короткий ответ заключается в том, что кажется, что правая ассоциативность может улучшить читаемость, делая то, что набирает программист, согласуется с тем, что на самом деле делает программа. Итак, если вы наберете '1 :: 2 :: 3', вы получите список (1, 2, 3) обратно, вместо того, чтобы получить список в совершенно другом порядке. - person Mike Spross; 22.07.2009
comment
@Mike: да, это то, что я добавлял к своему ответу, пока ты писал свои комментарии;) - person VonC; 22.07.2009
comment
VonC, не могли бы вы переместить краткий ответ в начало своего ответа? Я думаю, что это, как и правоассоциативные операторы, улучшит читабельность. :-) - person Daniel C. Sobral; 22.07.2009
comment
@ Даниэль: готово. Примечание: если у вас есть другие примеры правильных ассоциативных функций, опубликуйте их здесь с краткой причиной, почему их лучше использовать таким образом. Я бы проголосовал за это ;) - person VonC; 22.07.2009
comment
@Daniel: я бы также проголосовал за ответ с еще несколькими примерами. Пока у нас есть List cons (::) и ярлык List.foldLeft (/:) в качестве примеров правоассоциативных методов. Сначала я собирался сказать, что право-ассоциативный левый сгиб уродлив, но я уже усвоил, что это значит, поэтому он начинает казаться мне менее уродливым ;) - person Mike Spross; 22.07.2009
comment
Я не мог преодолеть ярлыки для сгиба. В любом случае, другие примеры можно найти в библиотеке Scalaz — Мартин пытается избежать превращения Scala в язык с большим количеством операторов, и под оператором я подразумеваю конкретно не слова как методы, поэтому вы не найдете много примеров в стандарте. библиотека. Однако я собираюсь предложить ~: для Regex. :-) - person Daniel C. Sobral; 22.07.2009
comment
@Vonc 1 :: 2 :: 3 - синтаксическая ошибка. Вы должны привязать это к списку справа. Следовательно, 1 :: 2 :: 3 :: Nil или head :: tail. - person Daniel C. Sobral; 22.07.2009
comment
Я хотел бы добавить, что: List(a, b, c).::(i) фактически создает список в форме (i, rest), остальные указывают на List(a, b, c). Вот почему prepend - это O (1). - person Phương Nguyễn; 17.07.2010

Какой смысл в возможности определять правоассоциативные методы?

Я думаю, что смысл правоассоциативного метода состоит в том, чтобы дать кому-то возможность расширить язык, что является точкой переопределения оператора в целом.

Перегрузка операторов — полезная вещь, поэтому Scala сказал: почему бы не открыть ее для любой комбинации символов? Скорее, зачем делать различие между операторами и методами? Теперь, как разработчик библиотеки взаимодействует со встроенными типами, такими как Int? В C++ она бы использовала функцию friend в глобальной области видимости. Что, если мы хотим, чтобы все типы реализовывали оператор ::?

Правая ассоциативность дает простой способ добавить оператор :: ко всем типам. Конечно, технически оператор :: является методом типа List. Но это также притворный оператор для встроенного Int и всех других типов, по крайней мере, если вы можете игнорировать :: Nil в конце.

Я думаю, что это отражает философию Scala, заключающуюся в том, чтобы реализовать как можно больше вещей в библиотеке и сделать язык гибким для их поддержки. Это дает возможность кому-то придумать SuperList, который можно назвать так:

1 :: 2 :: SuperNil

Несколько прискорбно, что правильная ассоциативность в настоящее время жестко запрограммирована только до двоеточия в конце, но я думаю, что это позволяет достаточно легко запомнить.

person Eugene Yokota    schedule 30.08.2009

Ассоциативность справа и слева играет важную роль при выполнении операций свертки списка. Например:

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

, потому что :: правоассоциативно.

person CodePredator    schedule 28.10.2013