Що є мотивацією для оцінки призначення Scala для Unit, а не присвоєного значення?


84

Що є мотивацією для оцінки призначення Scala для Unit, а не присвоєного значення?

Поширеною схемою програмування вводу-виводу є такі речі:

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

Але в Scala це неможливо, оскільки ...

bytesRead = in.read(buffer)

.. повертає Unit, а не нове значення bytesRead.

Здається цікавою річчю, яку можна залишити поза функціональною мовою. Цікаво, чому так було зроблено?


Девід Поллак опублікував деяку інформацію з перших рук, значною мірою схвалену коментарем самого Мартіна Одерського до своєї відповіді. Думаю, можна сміливо прийняти відповідь Поллака.
Daniel C. Sobral

Відповіді:


88

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


7
Чи є причина, чому компілятор Scala не міг перевірити, чи насправді використовується значення призначення, і відповідно створити ефективний байт-код?
Matt R

43
Це не так просто у присутності сетерів: кожен сетер повинен повернути результат, що дуже важко писати. Тоді компілятор повинен його оптимізувати, що важко зробити під час викликів.
Martin Odersky

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циклу, де ви оголошуєте та мутуєте змінну. Ви не можете (легко) мутувати стан 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

12

Це сталося в рамках Scala, що має більш "формально правильну" систему типу. Формально кажучи, доручення - це суто побічна заява, і тому воно має повернутися Unit. Це має кілька приємних наслідків; наприклад:

class MyBean {
  private var internalState: String = _

  def state = internalState

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

У state_=методі повертає значення Unit(як можна було б очікувати для інкубатора) саме тому , що повертає призначення Unit.

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


Дякую, Даніеле. Думаю, я би віддав перевагу, якби узгодженість полягала в тому, що обидва призначення та сеттери повертали значення! (Немає жодної причини, чому вони не можуть.) Я підозрюю, що поки що не перебираю нюанси таких понять, як "суто побічна заява".
Грем Леа

2
@Graham: Але тоді вам доведеться стежити за послідовністю та забезпечувати, щоб усі ваші установники, якими б складними вони не були, повертали значення, яке вони встановили. У деяких випадках це буде складно, а в інших випадках просто неправильно, я думаю. (Що б ви повернули у разі помилки? Null? - швидше ні. Жодного? - тоді ваш тип буде Option [T].) Я думаю, що важко бути з цим узгоджується.
Debilski

7

Можливо, це пов'язано з принципом розділення командного запиту ?

CQS, як правило, популярний на перетині ОО та стилів функціонального програмування, оскільки створює очевидну різницю між об'єктними методами, які мають або не мають побічних ефектів (тобто які змінюють об'єкт). Застосування CQS до призначень змінних робить це далі, ніж зазвичай, але застосовується та сама ідея.

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

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змінити список, вони повинні повернути тип одиниці, тим самим заважаючи нам створювати помилки, використовуючи другу форму, коли ми не повинні. Отже, наявність побічних ефектів також стає неявною у підписі методу.


4

Я думаю, це для того, щоб програма / мова не мала побічних ефектів.

Ви описуєте навмисне використання побічного ефекту, який у загальному випадку вважається поганим.


Хе. Скала без побічних ефектів? :) Крім того , уявіть собі ситуацію , як val a = b = 1(уявіть собі «чарівний» valперед b) проти val a = 1; val b = 1;.

Це не має нічого спільного з побічними ефектами, принаймні не в тому сенсі, який описаний тут: Побічний ефект (інформатика)
Фоєрмурмель

4

Використовувати призначення як логічний вираз - це не найкращий стиль. Ви виконуєте дві речі одночасно, що часто призводить до помилок. А випадкове використання "=" замість "==" уникається за допомогою обмеження Scalas.


2
Я думаю, це причина сміття! Коли ОП розміщував, код все ще компілюється та виконується: він просто не робить того, що ви могли б обгрунтовано очікувати. Це ще одна здобутка, а не одна менша!
oxbow_lakes

1
Якщо ви напишете щось на зразок if (a = b), воно не компілюється. Тож принаймні цієї помилки можна уникнути.
Дімон

1
ОП не використовував '=' замість '==', він використовував обидва. Він очікує, що присвоєння поверне значення, яке потім може бути використано, наприклад, для порівняння з іншим значенням (-1 у прикладі)
IttayD

@deamon: він компілюється (принаймні на Java), якщо a та b є логічними. Я бачив, як новачки потрапляють на цю пастку, використовуючи if (a = true). Ще одна причина віддати перевагу більш простому if (a) (і більш зрозумілому, якщо використовується більш значуще ім'я!).
PhiLho

2

До речі: я вважаю початковий трюк дурним навіть у Java. Чому б не щось подібне?

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

Звичайно, призначення відображається двічі, але принаймні bytesRead знаходиться в тому обсязі, до якого він належить, і я не граю з кумедними прийомами призначення ...


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більш неявно, без необхідності вводити його.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.