Какова мотивация присвоения Scala оценивать Unit, а не присвоенное значение?

84

Какова мотивация присвоения Scala оценивать Unit, а не присвоенное значение?

Обычный шаблон в программировании ввода-вывода - это делать что-то вроде этого:

while ((bytesRead = in.read(buffer)) != -1) { ...

Но в Scala это невозможно, потому что ...

bytesRead = in.read(buffer)

.. возвращает Unit, а не новое значение bytesRead.

Похоже, что интересно не использовать функциональный язык. Интересно, зачем это было сделано?

Грэм Ли
источник
Дэвид Поллак опубликовал некоторую информацию из первых рук, в значительной степени подтвержденную комментарием, который сам Мартин Одерски оставил на свой ответ. Я думаю, что можно смело принять ответ Поллака.
Дэниел С. Собрал

Ответы:

89

Я выступал за то, чтобы присваивания возвращали присвоенное значение, а не единицу. Мы с Мартином ходили по этому поводу, но его аргумент состоял в том, что размещение значения в стеке только для того, чтобы извлечь его в 95% случаев, было пустой тратой байтовых кодов и отрицательно сказывалось на производительности.

Дэвид Поллак
источник
7
Есть ли причина, по которой компилятор Scala не мог посмотреть, действительно ли используется значение присваивания, и соответственно сгенерировать эффективный байт-код?
Matt R
43
В присутствии сеттеров это не так просто: каждый сеттер должен возвращать результат, а писать это сложно. Затем компилятор должен оптимизировать его, что трудно сделать для нескольких вызовов.
Мартин Одерский,
1
Ваш аргумент имеет смысл, но Java и C # против этого. Я предполагаю, что вы делаете что-то странное с сгенерированным байтовым кодом, тогда как будет выглядеть присвоение в Scala, компилируемое в файл класса и декомпилированное обратно в Java?
Phương Nguyễn
3
@ PhươngNguyễn Отличие - Единый принцип доступа. В C # / Java сеттеры (обычно) возвращаются void. В Scala foo_=(v: Foo)должен возвращаться, Fooесли присваивание выполняется.
Алексей Романов
5
@Martin Odersky: как насчет следующего: сеттеры остаются void( Unit), назначения x = valueпереводятся в эквивалент x.set(value);x.get(value); компилятор исключает на этапах оптимизации getвызовы -вызовы, если значение не использовалось. Это могло быть долгожданным изменением в новом крупном (из-за обратной несовместимости) версии Scala и меньшим раздражением для пользователей. Что вы думаете?
Eugen Labun
20

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

Делается это разными способами. Например, у вас нет forцикла, в котором вы объявляете и изменяете переменную. Вы не можете (легко) изменить состояние whileцикла одновременно с проверкой условия, а это значит, что вам часто приходится повторять мутацию непосредственно перед ним и в конце. Переменные, объявленные внутри whileблока, не видны из whileусловия теста, что делаетdo { ... } while (...) гораздо менее полезными. И так далее.

Обходной путь:

while ({bytesRead = in.read(buffer); bytesRead != -1}) { ... 

Во что бы то ни стало.

В качестве альтернативного объяснения, возможно, Мартину Одерски пришлось столкнуться с несколькими очень уродливыми ошибками, возникающими из-за такого использования, и он решил запретить это в своем языке.

РЕДАКТИРОВАТЬ

Дэвид Поллак уже ответил с некоторыми реальными фактами, которые четко подтверждено тем фактом , что Одерски сам прокомментировал свой ответ, давая правдоподобность аргументу вопросы , связанные с производительностью , выдвинутой Поллак.

Дэниел С. Собрал
источник
3
Предположительно, forверсия цикла была бы такой: и for (bytesRead <- in.read(buffer) if (bytesRead) != -1это здорово, за исключением того, что она не будет работать, потому что ее нет foreachи withFilterнет!
oxbow_lakes 04
12

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

class MyBean {
  private var internalState: String = _

  def state = internalState

  def state_=(state: String) = internalState = state
}

В state_=методе возвращает значение Unit(как можно было бы ожидать для инкубатора) именно потому , что возвращает назначение Unit.

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

Даниэль Спивак
источник
Спасибо, Дэниел. Я думаю, я бы предпочел, чтобы последовательность была такова, что оба присваивания И сеттеры возвращали значение! (Нет никаких причин, по которым они не могут.) Я подозреваю, что пока не разбираюсь в нюансах концепций вроде «чисто побочного утверждения».
Грэм Ли
2
@Graham: Но тогда вам придется следить за согласованностью и гарантировать, что все ваши сеттеры, какими бы сложными они ни были, возвращали то значение, которое они установили. Я думаю, что в некоторых случаях это было бы сложно, а в других - просто неправильно. (Что бы вы вернули в случае ошибки? Null? - скорее, нет. None? - тогда ваш тип будет Option [T].) Я думаю, что трудно согласиться с этим.
Debilski
7

Возможно, это связано с разделением команд и запросов принципом ?

CQS имеет тенденцию быть популярным на пересечении объектно-ориентированного и функционального стилей программирования, поскольку он создает очевидное различие между объектными методами, которые имеют или не имеют побочных эффектов (т. Е. Изменяют объект). Применение CQS к присвоению переменных идет дальше обычного, но применима та же идея.

Короткая иллюстрация того , почему ОКК полезно: Рассмотрим гипотетический гибридный язык F / OO с Listклассом , который имеет методы Sort, Append, First, и Length. В императивном стиле объектно-ориентированного программирования можно написать такую ​​функцию:

func foo(x):
    var list = new List(4, -2, 3, 1)
    list.Append(x)
    list.Sort()
    # list now holds a sorted, five-element list
    var smallest = list.First()
    return smallest + list.Length()

В то время как в более функциональном стиле можно было бы написать что-то вроде этого:

func bar(x):
    var list = new List(4, -2, 3, 1)
    var smallest = list.Append(x).Sort().First()
    # list still holds an unsorted, four-element list
    return smallest + list.Length()

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

Использование CQS, однако, мы будем настаивать на том, что если Appendи Sortизменить список, они должны возвращать тип единицы, таким образом предотвращая нас от создания ошибок, используя вторую форму , когда мы не должны. Таким образом, наличие побочных эффектов также становится неявным в сигнатуре метода.

CA McCann
источник
4

Я предполагаю, что это сделано для того, чтобы программа / язык не имели побочных эффектов.

Вы описываете намеренное использование побочного эффекта, который в общем случае считается плохим.

Йенс Шаудер
источник
Хех. Scala без побочных эффектов? :) Кроме того , представьте себе ситуацию , как val a = b = 1(представьте себе «волшебный» valперед b) против val a = 1; val b = 1;.
Это не имеет ничего общего с побочными эффектами, по крайней мере, не в том смысле, который описан здесь: Побочный эффект (информатика)
Feuermurmel
4

Использование присваивания как логического выражения - не лучший вариант. Вы выполняете две вещи одновременно, что часто приводит к ошибкам. А случайное использование «=» вместо «==» избегается с помощью ограничения Scalas.

Deamon
источник
2
Думаю, это помешательство! Как было опубликовано OP, код все еще компилируется и запускается: он просто не делает того, чего вы могли разумно ожидать. Это еще одна ошибка, а не меньше!
oxbow_lakes 04
1
Если вы напишете что-то вроде if (a = b), оно не будет компилироваться. Так что, по крайней мере, этой ошибки можно избежать.
deamon
1
OP не использовал '=' вместо '==', он использовал оба. Он ожидает, что присвоение вернет значение, которое затем можно использовать, например, для сравнения с другим значением (-1 в примере)
IttayD
@deamon: он будет компилироваться (по крайней мере, на Java), если a и b являются логическими. Я видел, как новички попадались в эту ловушку, используя if (a = true). Еще одна причина предпочесть более простой if (a) (и более понятный при использовании более значимого имени!).
PhiLho
2

Между прочим: я считаю первоначальный трюк с while глупым даже в Java. Почему не что-нибудь подобное?

for(int bytesRead = in.read(buffer); bytesRead != -1; bytesRead = in.read(buffer)) {
   //do something 
}

Конечно, назначение появляется дважды, но, по крайней мере, bytesRead находится в той области видимости, к которой оно принадлежит, и я не играю с забавными трюками с назначениями ...

Landei
источник
1
Этот трюк является довольно распространенным, но обычно он появляется в каждом приложении, которое читает через буфер. И это всегда похоже на версию OP.
TWiStErRob
0

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

case class Ref[T](var value: T) {
  def := (newval: => T)(pred: T => Boolean): Boolean = {
    this.value = newval
    pred(this.value)
  }
}

Затем при ограничении, которое вам нужно будет использовать ref.valueдля доступа к ссылке впоследствии, вы можете записать свой whileпредикат как

val bytesRead = Ref(0) // maybe there is a way to get rid of this line

while ((bytesRead := in.read(buffer)) (_ != -1)) { // ...
  println(bytesRead.value)
}

и вы можете выполнить проверку bytesReadболее неявным образом, не вводя его.

Дебильски
источник