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

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

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

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

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

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

Мой вопрос заключается в том, какой смысл уметь определять правоассоциативные методы?

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

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

1 :: myList

лучше чем

myList :: 1

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

3 ответа

Решение

Короткий ответ заключается в том, что правильная ассоциативность может улучшить читаемость, делая тип программиста совместимым с тем, что на самом деле делает программа.
Итак, если вы напечатаете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)

что является одновременно:

  • более читабельный
  • более эффективный (O(1) для prependпротив O(n) для гипотетического append метод)

(Напоминание, выдержка из книги " Программирование в Scala")
Если метод используется в нотации оператора, такой как a * b, метод вызывается на левом операнде, как в a.*(b) - если имя метода не заканчивается двоеточием.
Если имя метода заканчивается двоеточием, метод вызывается с правым операндом.
Поэтому в 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's:::.


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

Это позволяет сохранить вид обычной левоассоциативной операции ('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)

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

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

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

Это прекрасно работает. Но вы не можете выполнить то же самое, используя операцию fold Left.

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

,так как :: является правоассоциативным.

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

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

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

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

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

1 :: 2 :: SuperNil

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

Другие вопросы по тегам