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

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

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

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

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

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

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

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

От моей (новички) точки зрения, я действительно не вижу, как

1 :: myList

лучше, чем

myList :: 1

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

Ответ 1

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

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

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

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


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

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

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

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


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

если вы рассматриваете функцию "добавить", лево-ассоциативную, тогда вы должны написать "oneTwo append 3 append 4 append 5".
Однако, если бы это было добавление 3, 4 и 5 к одному двум (которое вы предположили бы так, как оно написано), это будет O (N).
То же самое с "::", если это было для "append". Но это не так. Это на самом деле для "preend"

Это означает, что '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)

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

Ответ 2

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

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

В чем смысл определения право-ассоциативных методов?

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

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

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

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

1 :: 2 :: SuperNil

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