О выразительности и расширяемости на примере make

2011-05-10

@donnerpeter в своём блоге пишет о выразительности декларативных языков:

Seriously, unless you can prove your software will never be extended, don't program declaratively. (I'm speaking mostly to myself here but if this helps anyone else, you're welcome). The reason is obvious: declarative description is very pretty and concise as long as it's simple. But usually it becomes extremely complex to change things a bit if that bit isn't foreseen from the beginning. <...>

В качестве примера кода, в котором становится невозможно что-либо сделать при возрастании сложности, он приводит в пример Ant. Действительно, файлы сборки Ant могут становиться совершенно неуправляемыми и сложными для расширения при достаточно больших размерах проекта.

В качестве альтернативы @donnerpeter предлагает писать (императивный) код, который что-то делает, а не просто создаёт структуры из структур или преобразует структуры, которые в конечном счёте вынужден обрабатывать «реальный» код.

Попробую выразить своё мнение по этому вопросу.

Прежде всего, здесь связываются несвязанные вещи. С одной стороны, выразительность используемых моделей вычислений, с другой стороны, композиция и абстракция как способы управления сложностью. Почему эти вещи не связаны. С одной стороны, можно представить себе императивный Тьюринг-полный язык, в котором сложно и неудобно создавать и повторно использовать составные структуры данных и процедуры: это, например, ассемблер. Если бы средства композиции и абстракции были бы в нём хороши, мы бы писали на нём вместо C или Java. С другой стороны, можно представить язык с развитыми средствами абстракции, который, будучи не Тьюринг-полным и структурно-рекурсивным, не позволяет даже написать простого бесконечного цикла или произвести системный вызов. К примеру, язык Coq для автоматизированного доказательства теорем и корректности программ.

Проблема с применяемыми на практике декларативными языками в том, что их неправильно используют и поэтому начинают требовать а) выразительности там, где она не нужна; б) расширяемости там, где действие одного тула, решающего одну задачу, кончается. Не берусь приводить примеры про сложность в Ant, но зато приведу в качестве примера использование make, решающего похожие задачи.

Сначала о том, что выразительность при сборке не нужна. Это спорный пункт, т. к. всегда можно сказать: «я хочу делать при сборке вообще всё», но к этому я вернусь ниже. Итак, многие приводят Makefiles как пример плохо спроектированных средств, поскольку в «реальных» проектах они имеют такую большую сложность, что десятки килобайт этих файлов генерируют из других файлов инструменты типа GNU automake. Конечно, так будет, если иметь невнятную запутанную процедуру сборки с неправильным подходом к кросплатформенности, к подключению библиотек и т. д. В проектах, где процедура сборки чистая и почти кроссплатформенная, таких ложных сложностей сборки ПО просто нет. В качестве примеров хороших Makefiles можно посмотреть файлы сборки проектов suckless.org и файлы сборки библиотек языка Go. Просто не надо перекладывать все свои проблемы на инструменты сборки.

Ещё по поводу генерируемых Makefiles. Если уж процедура сборки столь сложна (хотя это и неправильно), то не лучше ли придумать стандарт сборки таких компонентов и их более высокоуровневое декларативное описание, как это, например, сделано в Python setuptools? И зачем использовать Makefile в качестве целевого кода, такого вот «декларативного ассемблера», когда лучше и для себя, и для пользователей реализовать интерпретацию этих описаний в императивном коде?

Теперь о границах расширяемости в инструментах сборки. Естественно, не стоит ожидать от языка описания графа зависимостей средств по выполнению программ общего назначения с доступом к ОС API. Ведь для этого есть другие средства и это не задача языка фиксации зависимостей — создавать каталоги и генерировать файлы. Однако поскольку make — это средство сборки, он включает в себя два языка: декларативный язык зависимостей и императивный язык команд оболочки. Последний даёт возможность вызывать что угодно в любых количествах и как раз на нём нужно расширять императивные действия в make.

Возвращаясь к декларативным языкам в целом, хочу заметить, что ограниченная выразительная сила языка может быть как минусом, так и плюсом. Если нет средств сделать в языке некую вещь и нельзя провзаимодействовать для этого с другим средством, то это минус. Но если за счёт ограничений можно находить больше ошибок на высоком уровне и упрощать понимание кода, то это, безусловно, положительный момент.