(Это эссе представляет собой введение к книге On Lisp.)
Давний принцип хорошего стиля программирования гласит, что функциональные элементы программы не должны быть слишком большими. Если какой-то компонент программы разрастается за пределы того, что можно легко охватить умом, он превращается в клубок сложности, скрывающий ошибки столь же легко, как большой город укрывает беглецов. Такое программное обеспечение будет трудно читать, трудно тестировать и трудно отлаживать.
В соответствии с этим принципом большую программу необходимо разбивать на части, и чем больше программа, тем сильнее её нужно делить. Как же делить программу? Традиционный подход называется проектированием сверху вниз (top-down design): вы говорите себе: «цель программы — делать вот эти семь вещей, поэтому я разделю её на семь основных подпрограмм. Первая подпрограмма должна выполнять четыре задачи, следовательно, у неё будет четыре собственных подпрограммы», и так далее. Этот процесс продолжается до тех пор, пока вся программа не достигнет нужного уровня детализации — когда каждая часть достаточно велика, чтобы делать что-то существенное, но достаточно мала, чтобы восприниматься как единое целое.
Опытные программисты на Lisp делят свои программы иначе. Наряду с проектированием сверху вниз они следуют принципу, который можно назвать проектированием снизу вверх (bottom-up design), — изменяя язык под задачу. В Lisp вы не просто пишете программу сверху вниз по направлению к языку, вы также достраиваете язык снизу вверх навстречу вашей программе. Работая над кодом, вы можете подумать: «Как было бы здорово, если бы в Lisp был такой-то оператор». И вы берете и пишете его. Затем вы понимаете, что использование нового оператора упростило бы структуру другой части программы, и так далее. Язык и программа развиваются вместе. Подобно границе между двумя воюющими государствами, рубеж между языком и программой многократно перекраивается, пока наконец не установится вдоль рек и хребтов — естественных рубежей вашей задачи. В итоге программа выглядит так, будто язык был создан специально для неё. А когда язык и программа идеально подходят друг другу, результатом становится ясный, компактный и эффективный код.
Стоит подчеркнуть, что проектирование снизу вверх вовсе не означает написание той же самой программы в другом порядке. Работая снизу вверх, вы обычно получаете совсем другую программу. Вместо единой монолитной программы у вас получается более богатый язык с более абстрактными операторами и более компактная программа, написанная на нём. Вместо балки перекрытия у вас получается арка.
В типичном коде, если абстрагироваться от рутинной технической работы (bookkeeping), остаток оказывается намного короче; чем выше вы надстраиваете язык, тем меньшее расстояние вам приходится преодолевать сверху вниз. Это дает несколько преимуществ:
Перекладывая больше работы на язык, проектирование снизу вверх позволяет получать программы меньшего размера и более гибкие. Более короткую программу не нужно делить на такое большое количество компонентов, а меньшее число компонентов означает, что код легче читать или изменять. Меньше компонентов — это также меньше связей между ними, а значит, меньше шансов допустить ошибку на стыках. Подобно тому как промышленные дизайнеры стремятся уменьшить количество движущихся деталей в механизме, опытные программисты на Lisp используют проектирование снизу вверх, чтобы уменьшить размер и сложность своих программ.
Проектирование снизу вверх способствует повторному использованию кода. Когда вы пишете две или более программ, многие из вспомогательных функций (utilities), созданных для первой, пригодятся и в последующих. Сформировав солидный фундамент из таких утилит, вы сможете писать новые программы, затрачивая лишь малую долю усилий, которые потребовались бы при разработке на чистом Lisp с нуля.
Проектирование снизу вверх делает программы легче для чтения. Абстракция такого типа требует от читателя понимания оператора общего назначения; в то время как функциональная абстракция требует от читателя понимания специализированной подпрограммы, решающей частную задачу. [1]
Поскольку этот подход заставляет вас постоянно выискивать паттерны в коде, работа снизу вверх помогает сделать архитектуру программы более ясной и продуманной. Если два далеких друг от друга компонента программы похожи по структуре, вы неизбежно заметите это сходство и, возможно, переработаете программу, сделав её проще.
Проектирование снизу вверх в некоторой степени возможно и в других языках, помимо Lisp. Всякий раз, когда вы встречаете библиотечные функции, происходит именно проектирование снизу вверх. Однако Lisp дает гораздо более широкие возможности на этом поприще, и расширение языка играет непропорционально большую роль в стиле программирования на Lisp — настолько, что Lisp оказывается не просто другим языком, а совершенно иным способом программирования.
Действительно, такой стиль разработки лучше подходит для программ, которые под силу написать небольшим группам. Однако одновременно он раздвигает границы того, что способна сделать небольшая команда. В книге The Mythical Man-Month Фредерик Брукс предположил, что продуктивность группы программистов не растет линейно с увеличением её численности. По мере роста команды продуктивность отдельных разработчиков падает. Опыт программирования на Lisp предлагает более оптимистичную формулировку этого закона: по мере уменьшения группы продуктивность отдельных разработчиков растет. Небольшая команда выигрывает, в относительном выражении, просто потому, что она меньше. А когда небольшая команда к тому же использует техники, которые открывает Lisp, она может победить безоговорочно.
Новое: Скачать On Lisp бесплатно.
[1] «Но никто не сможет прочитать программу, не разобравшись во всех ваших новых утилитах». О том, почему подобные утверждения обычно ошибочны, см. раздел 4.8.