Что хорошего в право-ассоциативных методах в Scala? - PullRequest
31 голосов
/ 22 июля 2009

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

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

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

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

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

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

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

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

1 :: myList

лучше

myList :: 1

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

Ответы [ 3 ]

37 голосов
/ 22 июля 2009

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

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

3 голосов
/ 28 октября 2013

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

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

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

3 голосов
/ 30 августа 2009

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

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

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

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

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

1 :: 2 :: SuperNil

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

...