Давайте посмотрим на следующий код:
a = 1
a = a + 1
print(a)
В среде ФП часто критикуют данный момент императивного программирования: «Как так может быть, что a = a + 1? Это всё равно что сказать „1 = 2“. В мутабельном присваивании нет смысла».
Здесь мы наблюдаем несовпадение обозначения: «равно» должно обозначать «равенство», когда на практике оно обозначает «присвоить». Я согласен с этой критикой и считаю, что это неудачная нотация. Но также мне известно, что в некоторых языках вместо a = a + 1
пишут выражение a := a + 1
. Почему же эта запись не является нормой?
На этот вопрос обычно отвечают «потому что так сделано в C». Но это похоже на перекладывание ответственности на кого-то другого: кто из нас знает, почему так сделано в C? Давайте разбираться вместе!
Большая четвёрка
В начале 1960-ых существовало четыре доминирующих высокоуровневых языка: COBOL, FORTRAN II, ALGOL-60, и LISP. В то время, программисты разбивали присваивание на два класса: инициализацию (initialization) — когда вы впервые определяете переменную, и переприсвоение (reassignment) — когда вы вы изменяется значение существующей переменной.
Итак, давайте добавим комментарии к нашему примеру на Python и получим следующий код:
a = 1 # Инициализация
a = a + 1 # Переприсвоение
print(a)
В то время люди не пользовались конкретно этими терминами для обозначения операций, но по сути это было как раз то, что делал каждый программист. В таблице ниже вы можете увидеть, какие из операторов использовались для каждого языка, и как выполнялась проверка на равенство.
Язык | Инициализация | Присваивание | Равенство |
---|---|---|---|
FORTRAN | = | = | .EQ. |
COBOL | INITIALIZE | MOVE [1] | EQUAL |
ALGOL | N/A | := | = |
LISP | let | set | equal |
В ALGOL не было отдельного оператора для инициализации — вместо этого вы создавали переменную определенного типа и затем использовали оператор для присвоения ей чего-либо. Вы могли написать integer x; x := 5;
, но не x := 5;
. Единственный язык из списка, который использовал =
для присваивания, это FORTRAN — и он выглядит подходящим кандидатом для ответа на наш вопрос.
Но мы-то с вами знаем, что C происходит от ALGOL; что, в свою очередь, означает, что по какой-то причине было решено отказаться от оператора присваивания :=
и изменить значение оператора =
с проверки на равенство…
ALGOL порождает CPL
ALGOL-60, скорее всего, является одним из самых влиятельных языков программирования в истории computer science. Вероятно, что при всём этом он также является одним из самых бесполезных языков. В основной спецификации языка намеренно не было предусмотрено никакой функциональности для ввода/вывода. Вы могли «захардкодить» вводы и измерять выводы, но если вам нужно было сделать с ними что-либо полезное, вам требовалось найти компилятор, который расширял бы базовый язык. ALGOL был спроектирован с целью исследования алгоритмов и поэтому он «ломался», когда вы пытались сделать на нём что-либо ещё.
Однако, он оказался настолько «крепким» языком, что другие захотели обобщить его для использования в бизнесе и в промышленности. Первую подобную попытку предприняли Кристофер Страчи и Кембриджский университет. Получившийся в итоге язык CPL добавил к функциональности ALGOL достаточное количество инновационных возможностей, о большей части которых мы в дальнейшем глубоко пожалели. Одной из них было определение с инициализацией, в котором переменная могла быть инициализирована и присвоена в одном выражении. Теперь вместо того, чтобы писать x; x := 5;
вы могли просто написать integer x = 5
. Просто супер!
Но здесь мы переключились с :=
на =
. Это происходит потому, что в CPL было три типа инициализации переменной:
- = означало инициализацию по значению.
- ≃ означала инициализацию по ссылке, поэтому если x ≃ y, то переприсваивание x также изменяет y. Но если вы написали x ≃ y + 1 и попробовали переприсвоить x, то программа бы «упала».
- ≡ означает инициализацию через подстановку, т.е. превращение x в функцию, не принимающую аргументов (niladic function), которая вычисляет правостороннее значение каждый раз, когда её используют. При этом нигде не объясняется, что должно случиться, если вы попробуете переприсвоить x — и я, поверьте, тоже не слишком хочу знать это.
Проблема: теперь =
использовался и для инициализации, и для равенства. К счастью, на практике в CPL эти варианты использования символа были четко разграничены: если вы где-либо писали =
, то было однозначно понятно, что имелось в виду.
Всего год спустя Кен Айверсон создаст APL, который станет использовать символ ←
для всех видов присваиваний. Поскольку на большинстве клавиатур такой клавиши нет и никогда не было, от него быстро откажется и сам автор — его следующий язык, J, тоже будет использовать для присваиваний символ =:
[2]. Однако, APL глубоко повлиял на S, который в свою очередь глубоко повлиял на R — вот почему <-
является предпочтительным оператором присваивания в R.
CPL порождает BCPL
CPL был замечательным языком, обладавшим всего одним небольшим недостатком: ни у кого не получалось написать его реализацию. Несколько человек смогли частично реализовать различные подмножества из его «фич», но этот язык оказался слишком большим и сложным для компиляторов той эпохи. Поэтому неудивительно, что Мартин Ричардс решил избавиться от ненужной сложности ящыка и создал BCPL. Первый компилятор BCPL появился в 1967 году… а первый компилятор CPL — лишь в 1970-м.
Среди многих других упрощений оказались и правила «трёх типов инициализации», которые приказали долго жить. Ричардс считал, что выражения-подстановки были вещью узкоспециальной, и их можно было заменить функциями (то же самое, по его мнению, касалось и присваиваний). Поэтому он совместил их всех в простое =
, за исключением наименований адресов глобальной памяти, которые использовали :
. Как и в случае CPL, =
представляло собой проверку на равенство. Для присвоения (reassignment), он использовал :=
— аналогично тому, как это сделали CPL и ALGOL. Многие из последовавших после языков также следовали этому соглашению: =
для инициализации, :=
для присваивания, =
для равенства. Но в широкие массы это пошло тогда, когда Никлаус Вирт создал Pascal — вот почему сегодня мы называем подобные обозначения «в стиле Pascal».
Насколько мне известно, BCPL был также первым «слабо типизированным» языком, поскольку единственным типом данных было машинное слово (data word)[3]. Это позволило сделать компилятор куда более портабельным за счет потенциального увеличения количества логических ошибок, но Ричардс надеялся на то, что улучшения в процессе и наименования с описанием позволят противостоять этому. Помимо все этого, именно в BCPL впервые появились фигурные скобки с целью определения блоков.
BCPL порождает B
Кен Томпсон хотел, чтобы BCPL мог выполняться на PDP-7. Несмотря на то, что у BCPL был «компактный компилятор», он всё ещё был в четыре раза больше, чем минимальный объем рабочей памяти на PDP-7 (16 кБ вместо 4 кБ). Поэтому Томпсону требовалось создать новый, более минималистичный язык. Также по личным эстетическим причинам он хотел минимизировать количество символов в исходном коде. Это и повлияло на дизайн языка B сильнее всего; вот почему в нём появились такие операторы, как ++ и --.
Если вы оставите в стороне использование поименованных адресов глобальной памяти, в BCPL всегда использовались следующие обозначения: =
для инициализации и :=
для переприсваивания (reassignment). Томпсон решил, что эти вещи можно совместить в единый токен, который можно использовать для всех видов присваивания, и выбрал =, поскольку оно было короче. Однако, это привнесло некоторую неоднозначность: если x
уже был объявлен, то чем было x = y
— присваиванием или проверкой на равенство? И это ещё не всё — в некоторых случаях предполагалось, что это это обе операции сразу! Поэтому он был вынужден добавить новый токен ==
как единую форму выражения смысла «равняется этому». Как выражался сам Томпсон:
Поскольку присваивание в типовой программе встречается примерно в два раза чаще, чем сравнение на равенство, уместно было сделать оператор присваивания вполовину короче.
За время, прошедшее между появлением BCPL и B, была создана Simula 67, первый объектно-ориентированный язык. Simula последовала соглашениям ALGOL о строгом разделении шагов инициализации и переприсвоения. Алан Кей примерно в это же время начал работу над Smalltalk, который добавил блоки, но последовал такому же синтаксису.
Томпсон (к которому присоединился Денис Ритчи) выпустил первую версию B примерно в 1969 году. Так что вплоть до 1971 года (примерно) большинство новых языков использовали для присваивания обозначение :=
.
B порождает C
… остальное – уже история.
Хорошо, есть ещё кое-что, о чём стоит рассказать. ML вышел год спустя, и, насколько мне известно, был первым языком, который привлек серьезное внимание к чистым функциям и отсутствию мутаций. Но в нем по-прежнему был спасательный круг в виде ссылочных ячеек (reference cells), которые можно было переприсваивать новым значениям при помощи оператора :=
.
Начиная с 1980, мы наблюдаем рост популярности новых императивных языков, ориентированных на корректность — в частности, Eiffel и Ada, оба из которых используют для операции присваивания символ :=
.
Если посмотреть на всю картину в целом, =
никогда не был «естественным выбором» для оператора присваивания. Почти все языки в семейном дереве ALGOL использовали вместо этого для присваивания :=
, возможно в силу того, что =
было столь тесно ассоциировано с равенством. В наши дни большинство языков использует = поскольку его использует C, и мы можем проследить эту историю до CPL, который представлял собой тот ещё бардак.
Примечания
1. В этом месте COBOL становится очень странным. У них есть несколько операторов, которые могут неявно мутировать, вроде ADD TO и COMPUTE. COBOL — плохой язык.
2. Мне нравится думать, что это было своеобразным приколом над :=
, хотя на самом деле этот оператор согласован с остальными частями языка, который использует .
и :
как суффиксы глаголов.
3. Позже в BCPL добавят ключевое слово для типа с плавающей запятой. И когда я говорю «позже», я имею в виду 2018 год.
Автор: Владимир Маслов