Мы родились в культуре с девизом «Никаких границ» или «Раздвигай границы», но на самом деле границы нам нужны. С ними мы становимся лучше, но это должны быть правильные границы.
Цензура ради качественной музыки
Когда перед нами встают внешние ограничения того, что можно сказать в песне, книге или фильме, то для передачи нужного смысла авторы должны использовать метафоры.
Возьмём для примера классическую песню Коула Портера 1928 года Let’s Do It (Let’s Fall in Love). Все мы понимаем, что подразумевается под «It» и это определённо не «давай влюбимся». Подозреваю, что автору пришлось добавить часть в скобках, чтобы избежать цензуры.
Перенесёмся в 2011 год и посмотрим на Slob on my Knob группы Three 6 Mafia. За исключением первого метафорического куплета всё остальное до отвращения очевидно.
Если отвлечься на минуту от художественности исполнения (или его отсутствия), то можно сказать, что в песне Коула Портера намёками говорится о том, что Three 6 Mafia вываливает на нас с невыносимыми подробностями, не оставляющими ничего для работы воображения.
Проблема заключается в том, что если не разделяете взглядов на занятия сексом, описываемых в текстах Three 6 Mafia, то посчитаете песню в лучшем случае вульгарной и совершенно не раскрывающей тему. А включив песню Коула Портера, слушатель может вызвать в воображении собственную фантазию.
То есть ограничения могут делать предмет более притягательным.
Акула сломалась
Изначально Стивен Спилберг планировал рассказать сюжет «Челюстей» через сцены с акулой. Но она постоянно ломалась. Бо́льшую часть времени съёмочная группа не могла показывать акулу — звезду этого фильма.
Лента, ставшая блокбастером, не существовала бы в своём нынешнем виде, если бы сложности с механикой не наложили ограничения на возможности Спилберга.
Почему этот фильм намного лучше того, в котором показывают акулу? Потому что каждый зритель самостоятельно заполняет пробелы с помощью своего воображения. Он вспоминает собственные фобии и проецирует их на экран. Поэтому страх ПЕРСОНАЛЕН для каждого зрителя.
Аниматорам этот принцип был известен уже давно. Включите звук падения за экраном, а затем покажите его последствия. В этом есть два преимущества. Во-первых, не нужно анимировать падение, во-вторых, падение происходит в сознании зрителя.
Почти все люди считают, что они видели то, как застрелили маму Бэмби. Но мы не только не видим, как в неё стреляют — мы даже никогда не видели её ПОСЛЕ выстрела. Но люди могут поклясться, что они видели обе сцены. Но этого НИКОГДА не показывали.
Итак, ограничения делают всё лучше. Гораздо лучше.
Возможности выбора повсюду
Представьте, что вы художник, и я прошу вас написать картину. Единственное, чего я прошу: «Нарисуйте мне что-нибудь красивое. То, что мне понравится».
Вы приходите в свою студию и сидите там, глядя на пустой холст. Вы бесконечно смотрите на него, и никак не можете начать писать. Почему?
Потому что вариантов слишком много. Вы можете нарисовать буквально что угодно. Я не поставил перед вами НИКАКИХ ограничений. Это явление называется парадоксом выбора.
Однако если бы я попросил нарисовать пейзаж, который мне понравится, я по крайней мере устранил бы половину бесконечных вариантов. Даже несмотря на то, что по-прежнему остаётся бесконечное количество вариантов, любые мысли о портрете будут быстро отметаться.
Если бы я пошёл дальше и сказал, что мне нравятся морские пейзажи и волны, разбивающиеся о берег во время золотого заката, то всё равно бы осталось бесконечное количество возможных картин, но эти ограничения на самом деле помогли бы вам думать о том, что нарисовать.
И пока неосознанно вы смогли бы начать писать морской пейзаж.
Итак, ограничения делают творчество проще.
Аппаратное обеспечение проще, чем программное
В «железе» никогда не бывает так, что транзистор или конденсатор использовался несколькими компонентами компьютера. Резисторы в схеме клавиатуры не могут использоваться графической картой.
Графическая карта обладает собственными резисторами, которыми управляет только она. Инженеры аппаратного обеспечения делают так не потому, что они хотят продать больше резисторов. Они делают так, потому что у них нет выбора.
Законы Вселенной гласят, что такую систему невозможно создать, не учинив хаоса. Вселенная задаёт правила разработчикам «железа», то есть ограничивает пределы возможного.
Такие ограничения делают работу с аппаратным обеспечением проще, чем работа с ПО.
В программах нет ничего невозможного
Теперь перейдём к ПО, в котором возможно почти всё. Ничто не мешает разработчику программного обеспечения использовать переменную в любой части программы. Такие переменные называются глобальными.
В языке ассемблера мы можем просто перейти к любой точке в коде и начать её выполнение. И это можно сделать в любой момент. Можно даже выполнять запись в данные, заставляя программу запускать непредусмотренный код. Такой метод используют хакеры, эксплуатирующие уязвимости типа «переполнение буфера».
Обычно операционная система ограничивает действия, которые программа может выполнить за своими пределами. Но никакие ограничения не накладываются на то, что она может делать с принадлежащими ей кодом и данными.
Именно отсутствие ограничений делает написание и поддержку ПО таким сложным делом.
Как правильно ставить ограничения в разработке ПО
Мы знаем, что при разработке ПО нужны ограничения, и по опыту знаем, что ограничения в других творческих профессиях могут пойти нам во благо.
Также мы знаем, что не можем позволять обществу случайным образом цензурировать наш код или ставить механические преграды, ограничивающие наши парадигмы. И мы не можем ждать от пользователей такого уровня квалификации, чтобы они ставили соответствующие ограничения в дизайне ПО.
Мы должны ограничивать себя сами. Но мы должны гарантировать, что эти ограничения пойдут всем во благо. Так какие же границы мы должны выбрать и как нам вообще принимать такие решения?
Чтобы ответить на этот вопрос, мы можем положиться на наш опыт и годы практики. Но самым полезным инструментом являются наши прошлые ошибки.
Боль от наших предыдущих действий, например, когда мы коснулись горячей плиты, говорит нам, какие ограничения мы должны наложить на себя, чтобы избавиться от таких мучений в будущем.
Let my People Go
Давным-давно люди писали программы, код которых прыгал из одного места в другое. Это называлось спагетти-кодом, потому что отслеживание подобного кода походило на наблюдение за одной макарониной в кастрюле.
Индустрия поняла, что такая практика контрпродуктивна и сначала запретила использование в коде конструкции GOTO тех языков, в которых она разрешалась.
Со временем, новые языки программирования полностью отказались от поддержки GOTO. Они стали называться языками структурного программирования. И сегодня все популярные высокоуровневые языки не содержат GOTO.
Когда это произошло, некоторые стали жаловаться, что новые языки слишком строги и что при использовании GOTO писать код проще.
Но победили более прогрессивно мыслящие, и мы должны быть благодарны им за отказ от такого разрушительного инструмента.
Прогрессивно мыслящие люди поняли, что код гораздо чаще читается, чем пишется или изменяется. То есть это может быть менее удобно для консерваторов, но в длительной перспективе жизнь с этим ограничением будет намного лучше.
Компьютеры по-прежнему могут выполнять GOTO. На самом деле, им это даже необходимо. Просто мы, как индустрия в целом, решила ограничить непосредственное использование их программистами. Все компьютерные языки компилируются в код, использующий GOTO. Но разработчики языков создали конструкции, использующие более упорядоченное ветвление, например, с помощью конструкции break, выполняющей выход из цикла for.
Индустрия программного обеспечения значительно выиграла от ограничений, поставленных разработчиками языков.
Надеваем кандалы
Так что же является GOTO сегодня и что разработчики языков готовят для нас, ничего не подозревающих программистов?
Чтобы ответить на этот вопрос, нам нужно рассмотреть те проблемы, с которыми мы сталкиваемся ежедневно.
- Сложность
- Многократное использование
- Глобальное изменяемое состояние
- Динамическая типизация
- Тестирование
- Крах Закона Мура
Как мы можем ограничить возможности программистов так, чтобы решить эти проблемы?
Сложность
Сложность растёт со временем. То, что изначально является простой системой, со временем эволюционирует в сложную. То, что начинается как сложная система, со временем эволюционирует в хаос.
Так как же нам ограничить программистов, чтобы помочь им снизить сложность?
Во-первых, мы можем заставить программистов писать код, полностью разбитый на небольшие части. Хотя это сложно, а может и невозможно полностью, мы можем создать языки, поощряющие такое поведение и вознаграждающие за него.
Многие функциональные языки программирования, особенно самые чистые, реализуют оба эти эффекта.
Написание функции, которое является вычислением, принуждает писать очень сильно разбитый на части код. Также это заставляет продумывать ментальную модель задачи.
Также мы можем наложить ограничения на то, что программисты могут делать в функциях, например, сделать все функции чистыми. Чистые функции — это те, у которых нет побочных эффектов, например, функции не могут получать доступ к данным, находящимся за их пределами.
Чистые функции работают только с переданными им данными, вычисляют свои результаты и передают их. Каждый раз, когда вы вызываете чистую функцию с одинаковыми входными данными, она ВСЕГДА будет выдавать одинаковые выходные данные.
Это делает работу с чистыми функциями гораздо логичнее, потому что все выполняемые ими задачи находятся целиком внутри самой функции. Также для них проще проводить юнит-тестирование, потому что они являются самодостаточными единицами. Если вычисления таких функций получаются затратными, то их результаты можно кэшировать. Если у вас будут одинаковые входные данные, то можно быть уверенным, что выходные тоже всегда одинаковы — идеальный сценарий для использования кэша.
Ограничивая программистов исключительно чистыми функциями, мы значительно ограничиваем сложность, потому что функции могут иметь только локальное влияние; кроме того, это помогает разработчикам естественным образом разбивать на части их программы.
Многократное использование
Индустрия программного обеспечения борется с этой проблемой почти с самого момента появления программирования. Сначала у нас были библиотеки, потом структурное программирование, а потом — объектно-ориентированное наследование.
Все эти подходы имеют ограниченную привлекательность и успех. Но есть один способ, который всегда работает и применялся почти каждым программистом — Copy/Paste, или «копипаста».
Если вы копируете и вставляете свой код, то делаете что-то не так.
Мы не можем запретить программистам копипастить, потому что они всё-таки пишут программы в виде текста, однако мы можем дать им кое-что получше.
В функциональном программировании есть стандартные практики, которые намного лучше копипасты, а именно функции высшего порядка, каррирование (карринг) и композиция.
Функции высшего порядка позволяют программистам передавать параметры, которые являются данными и функциями. В языках, не поддерживающих эту особенность, единственным решением является копирование и вставка функции с последующим редактированием логики. Благодаря функциям высшего порядка логику можно передавать как параметр в виде функции.
Каррирование (карринг) позволяет применять к функции по одному параметру за раз. Это позволяет программистам писать генерализированные версии функций и «запекать» некоторые из параметров для создания более специализированных версий.
Композиция позволяет программистам собирать функции как кубики Lego, позволяя им повторно использовать функционал, который они или другие встроили в конвейер, в котором данные переходят от одной функции к другой. Упрощённой формой этого являются конвейеры Unix.
Итак, хотя мы не можем избавиться от копипасты, мы можем сделать её необязательной благодаря поддержке языка и с помощью анализа кода, запрещающего её присутствие в кодовых базах.
Глобальное изменяемое состояние
Вероятно, это величайшая проблема в программировании, хотя многие и не осознают её как проблему.
Задавались ли вы когда-нибудь вопросом, почему чаще всего программные «баги» исправляются перезагрузкой компьютера или перезапуском проблемного приложения? Так происходит из-за состояния. Программа повреждает своё состояние.
Где-то в программе недопустимым образом изменяется состояние. Такие «баги» обычно одни из самым сложных в исправлении. Почему? Потому что их очень сложно воспроизвести.
Если вам не удаётся стабильно воспроизвести такой «баг», то вы не сможете найти способ его устранения. Вы можете проверить своё исправление и ничего не произойдёт. Но получилось ли так, потому что проблема устранена, или потому, что она пока не возникла?
Правильное управление состоянием — наиболее важный принцип, который нужно реализовать для обеспечения надёжности программы.
Функциональное программирование решает эту проблему, устанавливая ограничения для программистов на уровне языка. Программисты не могут создавать изменяемые переменные.
Поначалу кажется, что разработчики зашли слишком далеко, и пора бы поднять их на вилы. Но когда вы действительно работаете с такими системами, вы можете увидеть, что можно управлять состоянием, в то же время сделав все структуры данных неизменяемыми, то есть после того, как переменная получает значение, оно никогда не может измениться.
Это не значит, что состояние не может меняться. Это просто значит, что для этого необходимо передать текущее состояние в функцию, создающую новое состояние. Пока вы, любители хакинга, снова не начали точить свои вилы, могу уверить вас, что существуют механизмы оптимизации таких операций «за кулисами» с помощью Structural Sharing.
Учтите, что такие изменения происходят «под капотом». Как и в былые времена уничтожения GOTO, компилятор и выполняемая программа по-прежнему применяют GOTO. Они просто недоступны программистам.
Там, где должны возникнуть побочные эффекты, функциональное программирование имеет способы ограничения потенциально опасных частей программы. В хороших реализациях эти части кода явно помечены как опасные и отделены от чистого кода.
И когда в 98% кода отсутствуют побочные эффекты, портящие состояние баги могут оставаться только в оставшихся 2%. Это даёт программисту хороший шанс найти ошибки такого типа, потому что опасные части загнаны в загон.
То есть ограничивая программистов исключительно (или по большей мере) чистыми функциями, мы создаём более безопасные и надёжные программы.
Динамическая типизация
Есть ещё одна долгий и старый спор о статической типизации и динамической типизации. Статическая типизация — это когда тип переменной проверяется на этапе компиляции. После того, как вы зададите тип, компилятор помогает определить, используете ли вы его правильно.
Возражения против статической типизации заключаются в том, что она навешивает на программиста ненужную ношу и загрязняет код подробной информацией о типизации. И эта информация о типизации синтаксически «шумная», потому что находится рядом с определением функций.
При динамической типизации тип переменной никогда не задаётся и не проверяется на этапе компиляции. На самом деле, большинство языков с динамической типизации являются некомпилируемыми.
Возражения против динамической типизации заключаются в том, что несмотря на значительную очистку кода, программист не может отследить все случаи неправильного использования переменной. Их невозможно обнаружить, пока программа не будет запущена. Это означает, что несмотря на все усилия, ошибки типов добираются до этапа продакшена.
Так что же лучше? Поскольку здесь мы рассматриваем ограничение программистов, вы наверно ожидаете, что я буду выступать за статическую типизацию, несмотря на её недостатки. Вообще да, но почему бы нам не взять лучшее из обоих миров?
Оказывается, не все системы со статической типизацией созданы равными. Многие функциональные языки программирования поддерживают вывод типов, при котором компилятор может определить типы создаваемых вами функции на основании того, как вы их используете.
Это значит, что мы можем пользоваться статичной типизацией без излишнего задания типов. Рекомендации говорят нам, что типизация должна задаваться, а не определяться компилятором, но в таких языках, как Haskell и Elm, синтаксис типизации на самом деле не разрушает структуру и довольно полезен.
Нефункциональные, т.е. императивные языки со статической типизацией отягощают программиста заданием типов, не давая почти ничего взамен.
По сравнению с ними, системы типов Haskell и Elm на самом деле помогают программистам кодировать лучше и уведомляют их на этапе компиляции, если программа не будет работать правильно.
Итак, ограничивая программистов хорошей статической типизацией, компилятор может помочь в распознавании ошибок, определять типы и помогать в кодировании, а не отягощать разработчика многословной, навязчивой информацией о типах.
Тестирование
Написание кода тестов отравляет жизнь современного программиста. Очень часто разработчики тратят больше времени на написание кода тестов, чем на сам тестируемый код.
Написание кода тестов для функций, взаимодействующих с базами данных или веб-серверами сложно (если не невозможно) автоматизировать. Обычно существует два варианта.
- Не писать тесты
- Имитировать базу данных или сервер
Вариант 1 определённо не самый лучший, но многие люди выбирают его, потому что имитация сложных систем может быть более затратной по времени, чем написание модуля, который нужно протестировать.
Но если мы ограничим код исключительно чистыми функциями, то они не смогут непосредственно взаимодействовать с базой данных, потому что это может привести к побочным эффектам или мутациям. Нам по-прежнему нужно получать доступ к базе данных, но теперь наш слой опасного кода будет очень тонким интерфейсным слоем, в то время как бо́льшая часть модуля остаётся чистой.
Тестирование чистых функций намного проще. Но нам всё равно нужно писать код тестов, отравляющий нам жизнь. Или всё же нет?
Оказывается, что существуют программы для автоматического тестирования функциональных программ. Единственное, что должен предоставить программист — свойства, которых должны придерживаться функции, например, обратные функции. Автоматизированный тестер Haskell называется QuickCheck.
Итак, ограничивая большинство функций так, чтобы они были чистыми, мы делаем тестирование намного проще, а в некоторых случаях просто тривиальным.
Крах Закона Мура
Закон Мура — это на самом деле не закон, а практическое наблюдение, заключается в том, что вычислительная мощность компьютеров удваивается каждые два года.
Этот закон был справедлив более 50 лет. Но, к сожалению, мы достигли пределов современной технологии. И на разработку технологии создания компьютеров не на основе кремния могут потребоваться десятилетия.
А до этого момента наилучшим способом удвоения скорости компьютера является удвоение количества ядер, т.е. количества вычислительных «двигателей» центрального процессора. Но проблема не в том, что изготовители «железа» не могут дать нам больше ядер. Проблема заключается в аккумуляторах и ПО.
Удвоение вычислительной мощности означает удвоение потребляемого процессором питания. Это приведёт к ещё большему расходу батарей, чем сегодня. Аккумуляторные технологии сильно отстают от неутолимых аппетитов пользователей.
Поэтому вместо того, чтобы добавлять новые ядра, разряжающие аккумуляторы, нам, возможно, стоит оптимизировать использование уже имеющихся ядер. Именно здесь в дело вступает ПО. В современных императивных языках программирования очень сложно заставить программы выполняться параллельно.
Сегодня реализация параллелизма — тяжкая ноша для программиста. Программу необходимо разрезать вдоль и поперёк на параллельные части. Это непростая задача. И на практике, в таких языках, как JavaScript, программисты не могут управлять этим, потому что код не может выполняться параллельно, он однопоточный.
Но при использовании чистых функций не важно, в каком порядке они выполняются. Самое важное в них — доступность входных данных. Это означает, что компилятор или система выполнения может определять, когда и какие функции нужно выполнять.
Ограничиваясь только чистыми функциями, программист избавляется от заботы о параллелизме.
Функциональные программы смогут оптимальнее использовать преимущества многоядерных машин, не увеличивая сложность для разработчика.
Делать больше благодаря меньшим возможностям
Как мы видим, при установке правильных ограничений мы можем значительно улучшить наши художественные работы, дизайн и саму жизнь.
Разработчики аппаратного обеспечения сильно выиграли от естественных ограничений своих инструментов, которые упростили их работу и позволили за прошедшие десятилетия добиться потрясающего прогресса.
Мне кажется, что и для нас, разработчиков программного обеспечения, настало время ограничивать себя, чтобы достичь большего.
Автор: PatientZero