В спорах о «функциональном программировании против всех» звучат примерно одни и те же аргументы про типы, безопасность и верифицируемость. В конечном итоге дискуссия упирается в то, насколько плохи Fortran, С, С++ и как хороши Clojure, Haskell или Rust. И всегда чувствуется неприкрытый налёт «новое против старого».
Другая сторона — «всё против функционального программирования» — резонно спрашивает: если всё так замечательно, почему так мало реально полезного софта? Здесь претензия зеркальна: «старое против нового» и «молодые – глупые».
Но кое-что здесь не сходится. И Fortran, и принципы функционального программирования создал один и тот же человек. Поэтому спор «кто тут прав» теряет смысл — в такой постановке всегда прав Джон Бэкус. Это позволяет нам наконец задать содержательные вопросы.

Эта статья была написана в рамках курса главного редактора издательства «Ливрезон» Анатолия Рыжачкова «Невымученный текст». Курс посвящён технологичному созданию статей и сценариев, а также тому, как знание законов художественной литературы может существенно усилить нехудожественный текст. Подпишитесь на наш телеграм-канал, чтобы не пропустить анонсы новых курсов главреда.

В простом варианте это знакомо многим программистам: привычки и трюки из первого «серьёзного» языка надолго остаются в арсенале. В данном случае речь идёт о вычислительной модели, завязанной на состояния: программисты злоупотребляют возможностью сохранить состояние, хитро его передать и т.п. Это нередко выливается в гору временных переменных и «гейзенбаги», которые невозможно отладить. Избавиться от такой привычки сложно.
В частности, невозможность «легко и просто сунуть что-то в переменную» плавит лицо людям, которые пытаются освоить Lisp, Haskell да и в целом любой другой язык, построенный на модели, отличной от их первой «рабочей лошадки».
Бэкус своими глазами наблюдал, как зарождалась эта проблема, и отчасти ответственен за неё (привет, Fortran). Уверен, он знал, о чём говорит. И что бы мы сейчас ни придумали в качестве отвратительного примера, он видел нечто гораздо более ужасное. Но вот какое дело: Fortran появился как обобщение типовых конструкций из программ на машинном коде и ассемблере. То есть, грехи Fortran в методическом смысле — это грехи вычислительной модели машины фон Неймана. И, предлагая решение, Бэкус настаивал, что любой язык, сдизайненный в рамках этой модели, страдает от тех же проблем и в итоге учит программистов «плохим вещам».
Решение, которое он предлагал, — иная вычислительная модель. Та, что сейчас называется функциональной: программа как цепь функций-преобразователей. Задача программиста — не хитроумно разложить состояние по переменным, а построить алгоритм как последовательность трансформаций данных. Именно это мы видим сейчас в языках-наследниках Lisp, Haskell и других.
И с высоты сегодняшнего опыта мы ясно видим, что программисты вообще склонны злоупотреблять любыми трюками. Редкая программа на Lisp обходится без каши вроде (car (car (cdr …))), где злоупотребляют тем, что в Lisp что угодно есть список. Подобные примеры можно найти для любого языка, независимо от его вычислительной модели. В ту же копилку идиоматических злоупотреблений идут:
В конечном счёте, при всём многообразии, суть злоупотреблений и их последствий — в нарушении целостности представления данных. Это действительно методическая ошибка, но она никак не решается на уровне вычислительной модели. Если у программиста есть способ не вводить явное представление, то он почти наверняка им воспользуется. Как бы Java ни заставляла создавать классы для всего на свете, Haskell — типы, а Smalltalk — объекты, если можно всё свести к строке, числу и массиву, именно там данные и окажутся.
Таким образом, проблема до сих пор актуальна и понятна, но решать её приходится с совершенно другой — методической — стороны.
Это больн(ш)ая тема, актуальная во время выхода статьи Бэкуса и вновь активно всплывающая сегодня: можно ли, имея текст программы, доказать её свойства?
Проблема с состояниями, по утверждению Бэкуса, в том, что они слишком сильно зависят от предыдущих вычислений. И хотя формальное определение присваивания таки существует, практической пользы от него не так много. Нельзя просто засунуть 30-летнее C++-легаси в решатель (solver) и ожидать вердикта о корректности программы. Я лично знаю несколько человек, пытавшихся этого добиться. С каждым годом их становится всё меньше.
Более математичная модель, по мнению Бэкуса, будет более дружелюбна к подобному анализу. И здесь я не могу и не буду с ним спорить. Скорее всего, он прав. Некоторый современный опыт подтверждает возможность создания доказуемых программ, если идти обратным путём — конструировать программу через доказательство. Здесь можно указать на более насущную и, как мне кажется, более решаемую задачу — верификацию модели конструируемой программы.
Но я бы сосредоточил внимание на коё-чём ином.
«Формальные методы» — это не только доказательства корректности. Помимо ответа «да/нет», как мне кажется, должна существовать возможность вычислять количественные свойства программы. Одно такое свойство у нас есть — сложность алгоритмов. Может существовать какой-то ещё не открытый аналог закона Ома или формул ньютоновской механики, который позволил бы по условному «чертежу» определять свойства программы ещё до её написания.
Это открытая исследовательская тема, которую я встречал лишь у одного автора и только в формате «это, пожалуй, надо сделать». Её проглядел Бэкус, и её начисто игнорируют программисты и исследователи, считая, что «формальный метод» = «верификация». После этого называть программистов инженерами как-то даже смешно.
Этот аргумент интересен тем, что подобные споры полыхают до сих пор, но ни одна сторона не может внятно объяснить, в чём корень проблемы.
Бэкус отмечает, что последовательные (фон-неймановские) языки расширяются исключительно дизайнерами — за счёт раздувания базового фреймворка. Он видит проблему следующим образом.
Разворот первый. Задача диктует потребность в некотором языковом средстве, которого нет и не будет. Выкручивайся как знаешь.
Разворот второй. Дизайнер языка решает, какие средства добавить, исходя из своих представлений о потребностях. В итоге язык раздувается до чертей хреновой горой конструкций на все случаи жизни.
В своей критике Бэкус приводит в пример Pascal, но каждый программист в итоге наблюдал, как его изначально компактная, пусть и кособокая, рабочая лошадка превращается в монстра.
Я видел, как мой привычный Python раздулся горой зачастую бесполезных конструкций, которые только снижают портабельность кода. В сообществе C++ каждый новый стандарт — предмет драмы. Кому-то повезло меньше, и их язык сразу родился «фичастым уродцем», на освоение которого уходят безумные часы без вменяемого профита.
Оригинальная статья опубликована в 1977 году. То есть, нас предупреждали. Но какую альтернативу предложил Бэкус?
Определяемые пользователем языковые средства. То, что в Lisp обеспечивают макросы. Показательно, что для реализации этой идеи в рамках функциональной модели пришлось внести изменения в саму её основу — добавить работу с именами. Этот момент станет важным позже.
Бэкус утверждает, что встроить средства расширения синтаксиса в последовательные языки затруднительно. Однако опыт создания макросов для Python показывает, что фундаментальные ограничения накладываются, кажется, не вычислительной моделью, а дизайном самого языка. Получив доступ к AST, технически можно внедрять дополнительные средства, пусть мы и ограничены тем, что парсер Python сочтёт валидным набором токенов.
Вычислительная модель Бэкуса подобных ограничений не имеет. Если возможность расширения изначально заложена в базовую модель, то проклятие парсера базового фреймворка на нас не действует — ведь сам этот фреймворк создан с помощью тех же средств расширения.
И здесь мы подходим к очередному открытому вопросу: как наличие средств расширения синтаксиса влияет на дизайн языка? Надеюсь, ответ на него приведёт к действительно интересным решениям. У нас на руках уже есть пример Julia, а делать очередную засахаренную Java как минимум скучно.
До сих пор читатели из лагеря функциональщиков были солидарны со мной и с Бэкусом в теме состояний. Настало время их разозлить.
Бэкус прекрасно понимал одну важную вещь: без возможности оперировать состояниями вычислительная модель малополезна. Нельзя написать драйвер, не умея «крутить байты»; нельзя создать систему, где данные живут дольше рантайма, без возможности сохранять результаты (а это 99% как прикладного, так и системного софта).
Чтобы покрыть эти задачи, Бэкус добавляет в свою модель новые операции — сохранение и чтение именованного состояния.
Это не выглядит как «педаль назад», потому что в такой форме можно прибегать к состоянию только когда это необходимо, минимизируя проблемы вынужденного злоупотребления. Бэкус также высказывает надежду, что подобный подход позволит изолировать побочные эффекты и зашить в модель дополнительные ограничения.
Однако конкретно его попытка, к сожалению, такими свойствами не обладает. Вот программа из двух функций:
broken : x, y -> setv(y, (getv(x) - getv(y) / getv(y))
5, 5. dirty: <> → broken ° broken ° <x, y> ° setv(x, 5) ° setv(y, 5)
Её выполнение приведёт к ошибке из-за деления на ноль, но при этом состояние будет безвозвратно изменено. Где-то сейчас хаскеллист кричит: «МОНАДЫ!». И он прав, но в случае с Haskell дело не столько в монадах, сколько в транзакционной памяти (STM).
Опыт Haskell и Clojure наглядно показывает, что самозащита от таких проблем идёт не от свойств языка как такового, а от иных механизмов. Однако от проблемы «грязной» программы из примера выше системе Бэкуса не убежать.
Конечно, я несколько утрирую. Положить значение в именованную ячейку памяти — эта операция, разумеется, существует. Но вот в программе появляется конкурентность...
Управлять общим состоянием в этом случае титанически сложно, требуется огромное количество ручного контроля. Возникает множество «но», множество «нельзя» и практически неизбежные гейзенбаги. И в общем случае от этого никуда не деться. Можно, конечно, заявить: «Докажите ассоциативность операции». Но будем честны: практическая необходимость работы с персистентным состоянием существует — мы об этом говорили выше.
Clojure явно показывает, что решение возможно в частных случаях. В зависимости от конкретной модели управления состоянием и организации вычислений, можно определить конкретный вид «присваивания». Так, в Clojure существует аж 4 механизма работы с состоянием (описание утрировано и местами неточно):
(Источник)
Поэтому, говоря «присваивание», мы сваливаем в одну кучу совершенно разные ситуации, которые при всём желании не можем интерпретировать одинаково. Вот почему его «не существует».
Присваивание — не всегда присваивание.
Эта часть, пожалуй, ближе всего современному читателю, плотно работающему, например, с C++. Современные компиляторы далеко не всегда транслируют присваивание в тексте программы в команду записи в память. Во что выльется строка a = b, зависит от множества факторов.
Так же далеко не всегда «присваивание» — это выделение ячейки памяти под значение. Многие современные языки трактуют его как связывание имени с иммутабельным значением. То есть, это вопрос управления ссылками, а не перезаписи конкретной ячейки. Vars, Refs и Atoms в упомянутом выше Clojure работают именно так.
Бэкус эти аспекты полностью опустил.
Можно было бы сказать, что он просто не знал — тогда этого ещё не было. Однако как раз во время его исследований, лёгших в основу статьи, Хоар и Кнут обсуждали вопросы оптимизации при компиляции и первые успешные попытки в этой области. Так что Бэкус почти наверняка был в курсе.
Опустил намеренно? Случайно? Может, это было несущественно? Не знаю. И, как ни странно, это неважно. Несмотря на проявившуюся за последние годы сложность темы, это единственный из рассмотренных сегодня вопросов, по которому в индустрии уже сложилось некоторое понимание. Понимание того, что тема охрененно сложная и простых решений там нет и не будет.
До сих пор тон нашего разбора был несколько удручающим. С этой точки зрения работа Бэкуса выглядит скорее концептуальной, чем полезной в быту и производстве, а почти каждый поставленный вопрос остался без ответа. Но удивительно в статье Бэкуса то, что она полезна даже тому читателю, который плевать хотел на проблемы математиков и пресловутый «стиль фон Неймана».
Можно сказать, что его попытка провалилась. Функциональный стиль нашёл применение, но, как мы обсуждали, методических проблем не решил. Популярные языки продолжают абсурдно раздуваться. Настолько, что ради сугубо практических вопросов (воспроизводимость билдов, поддержка платформ) люди просто отказываются от «фич» и используют ограниченное подмножество языка. Но, сравнивая «тогда» и «сейчас», мы видим: хоть вопросы и открыты, стало примерно понятно, где искать решения. А это уже чрезвычайно ценно само по себе.
Мы не касались этого, но бóльшая часть статьи — это определения, выводы и примеры программ аж трёх вычислительных моделей: FP (чистые функциональные вычисления), FFP (фунциональщина с возможностью расширения) и AST (аппликативные вычисления с состояниями). И это как раз та часть, что полезна на практике в чистом виде.
К чести Бэкуса (и это сделано намеренно), математика там «по зубам старшекласснику». Разобраться в определениях и доказательствах не составит особого труда (привет одному лагерю функциональщиков — вы знаете, что сделали). Так же несложно реализовать любую из этих моделей, хоть полностью, хоть в виде ограниченного подмножества. Я сделал не один DSL, используя одну из них, — чаще всего обходился либо FP, либо FP+AST.
Остался последний, «горький» вывод. Надежды, которые мы возлагаем на «лучший» язык программирования, тщетны. Вычислительные модели могут демонстрировать полезные приёмы и учить хорошему или плохому, но они способны только подкрепить метод, а не дать его.
И главный метод, который вскрывает статья Бэкуса, — это не поиск «правильной» модели, а искусство декомпозиции задачи до уровня, где любая подходящая модель становится инструментом, а не прокрустовым ложем. Мы ждали волшебной таблетки, а получили учебник по трезвому инженерному анализу. И это, возможно, ценнее.
Концентрированная книга издательства LIVREZON складывается из сотен и тысяч проанализированных источников литературы и масс-медиа. Авторы скрупулёзно изучают книги, статьи, видео, интервью и делятся полезными материалами, формируя коллективную Базу знаний.
Пример – это фактурная единица информации: небанальное воспроизводимое преобразование, которое используется в исследовании. Увы, найти его непросто. С 2017 года наш Клуб авторов собрал более 80 тысяч примеров. Часть из них мы ежедневно публикуем здесь.
Каждый фрагмент Базы знаний относится к одной или нескольким категориям и обладает точной ссылкой на первоисточник. Продолжите читать материалы по теме или найдите книгу, чтобы изучить её самостоятельно.
📎 База знаний издательства LIVREZON – только полезные материалы.