В чем преимущество того, что оператор присваивания возвращает значение?

27

Я занимаюсь разработкой языка, который намереваюсь заменить как Javascript, так и PHP. (Я не вижу никаких проблем с этим. Не похоже, чтобы у любого из этих языков была большая база установки.)

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

x=1;          /* Assignment. */
if (x==1) {}  /* Comparison. */
x==1;         /* Error or warning, I've not decided yet. */
if (x=1) {}   /* Error. */

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

Либо это? Есть ли практическое использование возвращаемого значения оператора присваивания, которое нельзя было бы переписать тривиально? (Для любого языка, который имеет такое понятие.)

billpg
источник
12
JS и PHP не имеют большой "базы установки"?
mhr
47
@mri Я подозреваю, сарказм.
Энди Хант
12
Единственный полезный случай, который я могу вспомнить while((x = getValue()) != null) {}. Замены будут ужаснее, так как вам нужно будет использовать breakили повторить x = getValueназначение.
CodesInChaos
12
@ mri О, нет, я слышал, что эти два языка - просто тривиальные вещи без каких-либо значительных инвестиций. Как только несколько человек, которые настаивают на использовании JS, увидят мой язык, они переключатся на мой и больше никогда не будут писать ===. Я также уверен, что производители браузеров сразу же выпустят обновление, включающее мой язык наряду с JS. :)
billpg
4
Я хотел бы предложить вам, что если вы намерены улучшить существующий язык и хотите, чтобы он был широко принят, то 100% -ная обратная совместимость с существующим языком хорошая. См. TypeScript как пример. Если вы намереваетесь предоставить лучшую альтернативу существующему языку, у вас возникнет гораздо более сложная проблема. Новый язык должен решать существующую реалистическую проблему намного лучше, чем существующий, чтобы оплачивать стоимость переключения. Изучение языка - это инвестиция, и она должна окупиться.
Эрик Липперт

Ответы:

25

Технически, некоторый синтаксический сахар может стоить сохранить, даже если его можно заменить тривиально, если он улучшает читабельность некоторых общих операций. Но присваивание как выражение не подпадает под это. Опасность его опечатывания вместо сравнения означает, что он используется редко (иногда даже запрещено руководствами по стилю) и вызывает двойную реакцию при каждом его использовании. Другими словами, преимущества читабельности невелики по количеству и величине.

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

  • Java и C # сохраняют присваивание выражения, но устраняют упомянутую ловушку, требуя, чтобы условия вычислялись как логические. Похоже, это в основном работает хорошо, хотя люди иногда жалуются на то, что это запрещает такие условия, как if (x)вместо if (x != null)или в if (x != 0)зависимости от типа x.
  • Python делает присваивание правильным выражением вместо выражения. Предложения по изменению этого иногда достигают списка рассылки python-ideas, но мое субъективное впечатление состоит в том, что это происходит реже и генерирует меньше шума каждый раз по сравнению с другими «отсутствующими» функциями, такими как циклы do-while, операторы switch, многострочные лямбды, и т.п.

Однако, Python позволяет использовать один специальный случай, сопоставляя несколько имен сразу: a = b = c. Это считается оператором, эквивалентным b = c; a = bи иногда используемым, поэтому, возможно, стоит добавить и к вашему языку (но я бы не стал его использовать, так как это дополнение должно быть обратно совместимым).


источник
5
+1 за то, что поднимают, a = b = cа другие ответы на самом деле не поднимают.
Лев
6
Третье решение - использовать другой символ для назначения. Паскаль использует :=для назначения.
Брайан
5
@ Брайан: Действительно, как и C #. =это назначение, ==это сравнение.
Марьян Венема
3
В C # что-то вроде if (a = true)будет выдавать предупреждение C4706 ( The test value in a conditional expression was the result of an assignment.). GCC с C будет также бросить warning: suggest parentheses around assignment used as truth value [-Wparentheses]. Эти предупреждения могут быть заглушены дополнительным набором скобок, но они предназначены для того, чтобы явно указывать, что назначение было преднамеренным.
Боб
2
@delnan Просто несколько общий комментарий, но он был вызван «удалить ловушку, о которой вы говорите, требуя, чтобы условия оценивались в логические значения» - a = true действительно вычисляет в логическое значение и поэтому не является ошибкой, но также вызывает соответствующее предупреждение в C #.
Боб,
11

Есть ли практическое использование возвращаемого значения оператора присваивания, которое нельзя было бы переписать тривиально?

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

Обычные случаи обычно делают выражения компактными:

x = y = z;

имеет семантику в C # «преобразовать z в тип y, присвоить преобразованному значению y, преобразованное значение - это значение выражения, преобразовать его в тип x, присвоить x».

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

y = z;
x = y;

Аналогично с

M(x = 123);

быть сокращением для

x = 123;
M(x);

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

Я занимаюсь разработкой языка, который намереваюсь заменить как Javascript, так и PHP.

Если вы действительно хотите быть смелым и подчеркивать, что назначение - это утверждение, а не равенство, тогда мой совет: сделайте это четко выражением утверждения .

let x be 1;

Красный. Или

x <-- 1;

или даже лучше:

1 --> x;

Или еще лучше

1 → x;

Нет абсолютно никакого способа, с которым кто-либо из них будет перепутан x == 1.

Эрик Липперт
источник
1
Готов ли мир к использованию символов Unicode, отличных от ASCII, в языках программирования?
billpg
Как бы мне ни нравилось то, что вы предлагаете, одна из моих целей состоит в том, чтобы наиболее «хорошо написанный» JavaScript-код можно было перенести практически без изменений.
billpg
2
@billpg: Мир готов ? Я не знаю - был ли мир готов к APL в 1964 году, за десятилетия до изобретения Unicode? Вот программа на APL, которая выбирает случайную перестановку из шести чисел из первых 40: x[⍋x←6?40] APL требовалась собственная специальная клавиатура, но это был довольно успешный язык.
Эрик Липперт
@billpg: Семинар для программистов Macintosh использовал символы не ASCII для таких вещей, как теги регулярных выражений или перенаправление stderr. С другой стороны, у MPW было то преимущество, что Macintosh позволял легко вводить символы не ASCII. Я должен признаться в некотором недоумении относительно того, почему драйвер клавиатуры США не предоставляет каких-либо приличных средств для ввода любых символов, не входящих в ASCII. Ввод Alt-номера требует не только поиска кодов символов - во многих приложениях он даже не работает.
суперкат
Хм, а почему бы предпочесть назначить «вправо» как a+b*c --> x? Это выглядит странно для меня.
Руслан
9

Многие языки выбирают способ назначения присваивания вместо выражения, включая Python:

foo = 42 # works
if foo = 42: print "hi" # dies
bar(foo = 42) # keyword arg

и Голанг:

var foo int
foo = 42 # works
if foo = 42 { fmt.Printn("hi") } # dies

Другие языки не имеют присваивания, а скорее привязки с ограничением, например OCaml:

let foo = 42 in
  if foo = 42 then
    print_string "hi"

Однако letэто само выражение.

Преимущество разрешения присваивания состоит в том, что мы можем напрямую проверять возвращаемое значение функции внутри условного выражения, например, в следующем фрагменте Perl:

if (my $result = some_computation()) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}

Perl дополнительно ограничивает объявление только этим условным, что делает его очень полезным. Он также будет предупреждать, если вы назначите внутри условия без объявления новой переменной там - if ($foo = $bar)будет предупреждать, if (my $foo = $bar)не будет.

Выполнение присваивания в другом выражении обычно достаточно, но может вызвать проблемы с областью видимости:

my $result = some_computation()
if ($result) {
  say "We succeeded, and the result is $result";
}
else {
  warn "Failed with $result";
}
# $result is still visible here - eek!

Golang в значительной степени полагается на возвращаемые значения для проверки ошибок. Поэтому он позволяет условному элементу принимать оператор инициализации:

if result, err := some_computation(); err != nil {
  fmt.Printf("Failed with %d", result)
}
fmt.Printf("We succeeded, and the result is %d\n", result)

Другие языки используют систему типов для запрета не булевых выражений внутри условного выражения:

int foo;
if (foo = bar()) // Java does not like this

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

Теперь мы увидели различные механизмы защиты от случайного назначения:

  • Запретить присваивание как выражение
  • Используйте статическую проверку типов
  • Назначение не существует, у нас есть только letпривязки
  • Разрешить оператор инициализации, запретить присваивание в противном случае
  • Запретить присвоение внутри условного без объявления

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

Код без ошибок важнее краткого кода.

Амон
источник
+1 за «Запретить присваивание как выражение». Варианты использования для присваивания как выражения не оправдывают потенциальную возможность ошибок и проблем с читабельностью.
тыкай
7

Вы сказали: «Я подумал (с небольшими доказательствами, помимо моего личного опыта), что в подавляющем большинстве случаев это происходило, это действительно была операция сравнения».

Почему бы не исправить проблему?

Вместо = для присвоения и == для проверки на равенство, почему бы не использовать: = для присвоения и = (или даже ==) для равенства?

Заметим:

if (a=foo(bar)) {}  // obviously equality
if (a := foo(bar)) { do something with a } // obviously assignment

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

В то же время, если вы ДЕЙСТВИТЕЛЬНО хотели решить проблему, вы бы удалили крокод C, который утверждал, что логические значения были просто целыми числами с предопределенными символическими именами сахара. Сделайте их совсем другого типа. Тогда вместо того, чтобы сказать

int a = some_value();
if (a) {}

Вы заставляете программиста писать:

int a = some_value();
if (a /= 0) {} // Note that /= means 'not equal'.  This is your Ada lesson for today.

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

Джон Р. Штром
источник
2
(1) :=для назначения и =для равенства может решить эту проблему, но за счет отчуждения каждого программиста, который не вырос, используя небольшой набор неосновных языков. (2) Типы, отличные от того, что bools допускается в условиях, не всегда связаны со смешением bools и целых чисел, достаточно дать истину / ложную интерпретацию другим типам. Более новый язык, который не боится отклоняться от C, сделал это для многих типов, кроме целых (например, Python считает пустые коллекции ложными).
1
И в отношении бритвенных лезвий: они служат случаю использования, который требует остроты. С другой стороны, я не уверен, что программирование хорошо требует назначения переменных в середине вычисления выражения. Если бы существовал простой, нетехнологичный, безопасный и экономически эффективный способ заставить волосы на теле исчезать без острых краев, я уверен, что лезвия бритвы были бы смещены или, по крайней мере, сделаны намного реже.
1
@delnan: Один мудрец однажды сказал: «Сделай это как можно проще, но не проще». Если ваша цель состоит в том, чтобы устранить подавляющее большинство ошибок a = b и a == b, ограничив область условных тестов булевыми значениями и исключив правила преобразования типов по умолчанию для <other> -> boolean, вы получите практически все там. На этом этапе, если (a = b) {} синтаксически допустим, только если a и b оба являются булевыми, а a является допустимым lvalue.
Джон Р. Штром
Сделать присваивание оператором, по крайней мере, так же просто, как, возможно, даже проще, чем предлагаемые вами изменения, и достичь, по крайней мере, так же, возможно, даже больше (даже не допускается if (a = b)lvalue a, логическое a, b). На языке без статической типизации это также дает намного лучшие сообщения об ошибках (во время анализа против времени выполнения). Кроме того, предотвращение «ошибок a = b и a == b» может быть не единственной важной задачей. Например, я также хотел бы разрешить код, как if items:означать if len(items) != 0, и что я должен был бы отказаться, чтобы ограничить условия булевыми значениями.
1
@delnan Pascal это не основной язык? Миллионы людей изучали программирование с использованием Паскаля (и / или Модула, который происходит от Паскаля). А Delphi до сих пор широко используется во многих странах (возможно, не в вашей).
jwenting
5

На самом деле, чтобы ответить на вопрос, да, есть множество применений этого, хотя они немного нишу.

Например в Java:

while ((Object ob = x.next()) != null) {
    // This will loop through calling next() until it returns null
    // The value of the returned object is available as ob within the loop
}

Альтернатива без использования встроенного присваивания требует obопределенного вне области цикла и двух отдельных положений кода, которые вызывают x.next ().

Уже упоминалось, что вы можете назначить несколько переменных за один шаг.

x = y = z = 3;

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

Тим Б
источник
Будет ли в то время как условие цикла освободить и создать новый ob объект с каждым циклом?
user3932000
@ user3932000 В этом случае, вероятно, нет, обычно x.next () выполняет итерации по чему-то. Вполне возможно, что это возможно, хотя.
Тим Б
1

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

Другими словами, сделайте это законным:

a=b=c=0;

Но сделайте это незаконным:

if (a=b) ...
Брайан Оукли
источник
2
Это кажется довольно специальным правилом. Создание назначения оператора и расширение его для разрешения a = b = cкажется более ортогональным и более простым для реализации. Эти два подхода не согласны с присваиванием в выражениях ( a + (b = c)), но вы их не приняли, поэтому я предполагаю, что они не имеют значения.
«Легко реализовать» не должно быть предметом рассмотрения. Вы определяете пользовательский интерфейс - ставьте потребности пользователей на первое место. Вам просто нужно спросить себя, помогает ли это поведение пользователю.
Брайан Окли
если вы запрещаете неявное преобразование в bool, вам не нужно беспокоиться о назначении в условиях
ratchet freak
Проще реализовать был только один из моих аргументов. А как насчет отдыха? С точки зрения пользовательского интерфейса, я мог бы добавить, что IMHO непоследовательный дизайн и специальные исключения обычно мешают пользователю уловить и усвоить правила.
@ratchetfreak у вас все еще может быть проблема с назначением реальных bools
jk.
0

Судя по всему, вы находитесь на пути создания довольно строгого языка.

Имея это в виду, заставляя людей писать:

a=c;
b=c;

вместо того:

a=b=c;

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

if (a=b) {

когда они собирались сделать:

if (a==b) {

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

Тем не менее, существуют ситуации, когда:

a=c;
b=c;

не значит что

if (a==b) {

будет правдой.

Если c на самом деле является функцией c (), то она может возвращать разные результаты при каждом вызове. (это может быть слишком дорого в вычислительном отношении ...)

Аналогично, если c является указателем на отображаемое в памяти оборудование, то

a=*c;
b=*c;

оба могут быть разными, а также могут иметь электронные эффекты на аппаратное обеспечение при каждом чтении.

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

Майкл Шоу
источник
4
Это a = b = cне то a = c; b = c, что надо b = c; a = b. Это позволяет избежать дублирования побочных эффектов, а также сохраняет изменения aи bв том же порядке. Кроме того, все эти аппаратные аргументы являются глупыми: большинство языков не являются системными языками и не предназначены для решения этих проблем и не используются в ситуациях, когда эти проблемы возникают. Это вдвойне относится к языку, который пытается вытеснить JavaScript и / или PHP.
Делнан, вопрос не в том, являются ли они надуманными примерами. Все еще остается точка зрения, что они показывают виды мест, где написание a = b = c является обычным явлением, а в случае с аппаратным обеспечением считается хорошей практикой, о чем просил OP. Я уверен, что они смогут рассмотреть их отношение к их ожидаемой среде
Майкл Шоу
Оглядываясь назад, моя проблема с этим ответом состоит не только в том, что он фокусируется на сценариях использования системного программирования (хотя это было бы достаточно плохо, как написано), а в том, что он основывается на предположении неправильного переписывания. Эти примеры не являются примерами мест, где a=b=cобычно / полезно, они являются примерами мест, где нужно учитывать порядок и количество побочных эффектов. Это совершенно независимо. Правильно переписать связанное назначение, и оба варианта одинаково верны.
@delnan: значение r преобразуется в тип bв одном временном файле, а значение преобразуется в тип aв другом временном. Относительное время, когда эти значения фактически сохраняются, не определено. С точки зрения языкового дизайна, я считаю разумным требовать, чтобы все lvalues ​​в операторе множественного присваивания имели совпадающий тип, и, возможно, также потребовать, чтобы ни одно из них не было изменчивым.
суперкат
0

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

Python не имеет этого; у него есть выражения и утверждения, назначение - утверждение. Но поскольку Python определяет lambdaформу как одно параметризованное выражение , это означает, что вы не можете назначать переменные внутри лямбда- выражения . Иногда это неудобно, но не является критической проблемой, и это мой единственный недостаток в моем опыте - иметь назначение в Python.

Один из способов, позволяющих присваивать или, точнее, эффект присваивания быть выражением, не представляя возможности для if(x=1)несчастных случаев, которые имеет C, - это использовать LISP-подобную letконструкцию, такую, (let ((x 2) (y 3)) (+ x y))которая в вашем языке может оцениваться как 5. Использование letэтого способа технически не обязательно должно быть назначением на вашем языке, если вы определяете letкак создание лексической области видимости. Определенный таким образом, letконструкция может быть скомпилирована так же, как конструирование и вызов вложенной функции замыкания с аргументами.

С другой стороны, если вы просто озабочены if(x=1)регистром, но хотите, чтобы присваивание было выражением, как в C, возможно, достаточно будет просто выбрать другие токены. Назначение: x := 1или x <- 1. Сравнение: x == 1. Синтаксическая ошибка: x = 1.

wberry
источник
1
letотличается от присваивания большим количеством способов, чем технически введение новой переменной в новую область видимости. Начнем с того, что он не влияет на код за пределами letтела и, следовательно, требует дальнейшего вложения всего кода (что должно использовать переменную), что является существенным недостатком кода с интенсивным присваиванием. Если идти по этому пути, set!будет лучшим аналогом Lisp - совершенно не похоже на сравнение, но не требует вложения или новой области видимости.
@delnan: Я хотел бы видеть комбинацию синтаксиса объявления-и-назначения, которая запрещает переназначение, но допускает повторное объявление, при условии соблюдения правил, согласно которым (1) повторное объявление будет разрешено только для идентификаторов объявления-и-назначения, и (2 ) redeclaration будет "undeclare" переменная во всех входящих областях. Таким образом, значение любого действительного идентификатора будет тем, что было назначено в предыдущем объявлении этого имени. Это может показаться немного более приятным, чем добавление областей видимости для переменных, которые используются только для нескольких строк, или необходимость формулировать новые имена для каждой временной переменной.
суперкат
0

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

Верно. В этом нет ничего нового, все безопасные подмножества языка Си уже сделали такой вывод.

MISRA-C, CERT-C и т. Д. На все запреты присваиваются внутри условий просто потому, что это опасно.

Не существует случая, когда код, основанный на присваивании внутри условий, нельзя переписать.


Кроме того, такие стандарты также предупреждают против написания кода, который опирается на порядок оценки. Многократные назначения в одной единственной строке x=y=z;- такой случай. Если строка с несколькими присваиваниями содержит побочные эффекты (вызов функций, доступ к переменным переменным и т. Д.), Вы не можете знать, какой побочный эффект произойдет первым.

Между оценками операндов нет последовательностей. Таким образом, мы не можем знать, yоценивается ли подвыражение до или после z: это неопределенное поведение в C. Таким образом, такой код потенциально ненадежен, непереносим и не соответствует упомянутым безопасным подмножествам C.

Решением было бы заменить код на y=z; x=y;. Это добавляет точку последовательности и гарантирует порядок оценки.


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


источник