Что хорошего в право-ассоциативных методах в 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
Несколько прискорбно, что правильная ассоциативность в настоящее время жестко запрограммирована только в конце двоеточия, но я думаю, что это достаточно легко запомнить.