Яка вигода від того, щоб оператор присвоєння повернув значення?


27

Я розробляю мову, яку я маю намір замінити як Javascript, так і PHP. (Я не можу бачити жодних проблем з цим. Це не так, як у будь-якої з цих мов є велика база встановлення.)

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

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

Я знаю, що це означало б, що ті однолінійні функції, які так люблять люди С, більше не працюватимуть. Я подумав (маючи мало доказів, ніж мій особистий досвід), що переважна більшість разів це сталося, це було дійсно призначене для порівняння.

Або це? Чи є практичне використання зворотного значення оператора призначення, яке неможливо було б тривільно переписати? (Для будь-якої мови, яка має таке поняття.)


12
JS і PHP не мають великої "встановити базу"?
13:00

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

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


5
+1 за те, a = b = cщо інші відповіді насправді не відповідають.
Лев

6
Третя резолюція - використовувати інший символ для призначення. Паскаль використовує :=для призначення.
Брайан

5
@Brian: Дійсно, як і C #. =це призначення, ==є порівняння.
Мар'ян Венема

3
У C # щось подібне if (a = true)викине попередження C4706 ( The test value in a conditional expression was the result of an assignment.). GCC з C також буде кидати a 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 потрібно шукати коди символів - у багатьох додатках це навіть не працює.
supercat

Гм, чому б ви вважали за краще привласнювати "праворуч" 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) Типи, окрім болів, що дозволяється в умовах, не завжди пов'язані зі змішуванням булів та цілих чисел, достатньо дати правдиву / помилкову інтерпретацію іншим типам. Новіші мови, які не бояться відступати від C, робили це для багатьох типів, крім цілих чисел (наприклад, Python вважає порожні колекції помилковими).

1
Що стосується лез для бритви: вони служать випадком використання, який вимагає різкості. З іншого боку, я не впевнений, що програмування добре вимагає присвоєння змінним в середині оцінки виразів. Якби був простий, низькотехнологічний, безпечний і економічно ефективний спосіб змусити волосся на тілі зникнути без гострих країв, я впевнений, що леза бритви були б зміщені або, принаймні, набагато рідше.

1
@delnan: Мудрець одного разу сказав: "Зробіть це якомога простіше, але не простіше". Якщо ваша мета - усунути переважну більшість помилок a = b проти a == b, обмеження домену умовних тестів булевими і виключення правил конверсії типу за замовчуванням для <bother> -> boolean отримує вас майже на всьому шляху там. У той момент, якщо (a = b) {} є лише синтаксично законним, якщо a і b є булевими, а a - юридичним значенням.
Джон Р. Стром

Зробити заяву про присвоєння принаймні настільки ж просто, як - можливо, навіть простіше, ніж - запропоновані вами зміни, і досягти хоча б стільки ж - можливо, навіть більше (навіть не дозволяє if (a = b)lvalue a, boolean a, b). Мовою, яка не має статичного набору тексту, вона також дає набагато кращі повідомлення про помилки (за часом розбору та часу виконання). Крім того, запобігання помилкам "a = b vs. 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об'єкта з кожним циклом?
користувач3932000

@ user3932000 У такому випадку, мабуть, ні, зазвичай x.next () повторюється над чимось. Звичайно, можливо, що він міг би хоч.
Тім Б

1

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

Іншими словами, зробіть це законним:

a=b=c=0;

Але зробіть це незаконним:

if (a=b) ...

2
Це виглядає як досить спеціальне правило. Здійснення завдання присвоєння і розширення його дозволеного a = b = cздається більш ортогональним і простішим також для реалізації. Ці два підходи не погоджуються щодо присвоєння у виразах ( a + (b = c)), але ви не стали на них сторонами, тому я припускаю, що вони не мають значення.

"Легкий у здійсненні" не повинен дуже враховувати. Ви визначаєте користувальницький інтерфейс - поставте потреби користувачів на перше місце. Вам просто потрібно запитати себе, чи допомагає така поведінка користувачеві чи перешкоджає цьому.
Брайан Оуклі

якщо ви забороните неявну конверсію в bool, вам не доведеться турбуватися про присвоєння в умовах
храповик виродка

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

@ratchetfreak у вас все ще можуть виникнути проблеми із призначенням фактичних випусків
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 є загальним, а в апаратному випадку вважаються хорошою практикою, як вимагає ОП. Я впевнений, що вони зможуть врахувати свою приналежність до очікуваного оточення
Майкл Шоу

Озираючись назад, моя проблема з цією відповіддю полягає не в першу чергу в тому, що вона зосереджена на випадках використання системного програмування (хоча це було б погано, способом написання), а в тому, що вона спирається на припущення про неправильне переписування. Приклади не є прикладами місць, де a=b=cє загальним / корисним, вони є прикладами місць, де слід потурбуватися про порядок та кількість побічних ефектів. Це абсолютно незалежно. Перепишіть ланцюгову задачу правильно, і обидва варіанти однаково правильні.

@delnan: Rvalue перетворюється на тип b в одній темп, і це перетворюється на тип aв інший temp. Відносні терміни, коли ці значення фактично зберігаються, не визначено. З точки зору дизайну мови, я вважаю доцільним вимагати, щоб усі значення в операторі з множинним призначенням мали тип відповідності та, можливо, вимагати, щоб жоден з них не мінливий.
supercat

0

Найбільша користь для моєї присвоєння завданням є вираз - це те, що це дозволяє вашій граматиці бути простішою, якщо однією з ваших цілей є те, що "все є вираженням" - зокрема, LISP.

У Python цього немає; він має вирази та висловлювання, присвоєння є заявою. Але оскільки Python визначає lambdaформу як єдиний параметризований вираз , це означає, що ви не можете призначити змінні всередині лямбда. Часом це незручно, але не є критичним питанням, і це є єдиним недоліком мого досвіду в тому, щоб мати завдання - це заява в Python.

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

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


1
letвідрізняється від присвоєння більшою мірою, ніж технічно вводити нову змінну в нову область. Для початківців це не впливає на код поза letтілом тіла, а тому вимагає додаткового введення всього коду (для чого слід використовувати змінну), що є значним недоліком коду, який присвоює важкі завдання. Якщо хтось повинен був спуститися цим маршрутом, set!був би кращим аналогом Ліспа - абсолютно на відміну від порівняння, але не потребуючи вкладення чи нового розмаху.

@delnan: Я хотів би побачити комбінацію синтаксису оголосити та призначити, який би заборонив перепризначення, але дозволив би передекларацію, з дотриманням правил, які (1) повторна декларація була б законною лише для ідентифікаторів оголошення та призначення, і (2) ) передекларація дозволить "визначити" змінну у всіх областях, що додаються. Таким чином, значенням будь-якого дійсного ідентифікатора було б те, що було визначено в попередній декларації цього імені. Здавалося б, це трохи приємніше, ніж додавати блоки масштабування змінних, які використовуються лише для декількох рядків, або потрібно формулювати нові імена для кожної тимчасової змінної.
supercat

0

Я знаю, що це означало б, що ті однолінійні функції, які так люблять люди С, більше не працюватимуть. Я подумав (маючи мало доказів, ніж мій особистий досвід), що переважна більшість разів це сталося, це було дійсно призначене для порівняння.

Справді. Це нічого нового, всі безпечні підмножини мови С вже зробили такий висновок.

MISRA-C, CERT-C і так далі всі заборони присвоювати всередині умов просто тому, що це небезпечно.

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


Крім того, такі стандарти також застерігають від написання коду, який спирається на порядок оцінювання. Кілька завдань в одному рядкуx=y=z; є таким випадком. Якщо рядок з декількома призначеннями містить побічні ефекти (викличні функції, доступ до змінних змінних тощо), ви не можете знати, який побічний ефект виникне першим.

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

Рішенням було б замінити код на y=z; x=y; . Це додає бал послідовності та гарантує порядок оцінювання.


Отже, виходячи з усіх проблем, спричинених у C, будь-яка сучасна мова добре допоможе як забороняти присвоєння всередині умов, так і безліч призначень в одному рядку.

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