Архив 20 мая 2013

Адские самодельные языки программирования

У [info]metaclass наметился срачик по поводу “самодельных” языков программирования. Естественно, вся прогрессивная общественность в едином порыве осуждает эту порочную практику. Я же хочу предложить взгляд на проблему с несколько другой стороны – а именно, начать с замечания о том, что деятельность программиста как нельзя лучше описывается фразой “разработка языков програмирования” и разобраться, к чему приведет эта посылка.

Разумеется, что она выглядит несколько провокационно. Поэтому придется ее несколько прокомментировать. Начну со своих личных впечатлений от изучения некоего подмножества Computer Science на мехмате МГУ. Студенты мехмата по отношению к этой науке делятся на две части – первым она дается легко и непринужденно, вторым – с большим трудом (но зачет эти вторые все же как-то сдают). Отличить первых от вторых можно по тексту тех программ, которые они пишут. В большинстве случаев студент, которому программирование дается легко, определит в программе какое-то количество функций и процедур, иногда – пару-тройку макросов для часто встречающихся конструкций, попытается разбить задачу на независимые части. Его не любящий программирование коллега скорее обойдется единственной функцией main().

В чем различие? Первый студент просто владеет главным методом процедурного программирования – “реши, какие требуются процедуры, используй наилучшие доступные алгоритмы”. Как только определен набор процедур для решения задачи – то мы просто пользуемся ими наравне со “штатными” средствами языка программирования. Например, в типичной для третьего курса мехмата задаче “обратить матрицу методом Гаусса” можно выделить следующие элементарные действия с матрицей – “вычесть из строки n строку m, умноженную на какой-то коэффициент” и “поменять местами строки n и m“. Если мы храним матрицу, как массив размера N*M, то логично будет написать макрос, вычисляющий индекс элемента an,m в этом массиве. В общем, реализовав несколько таких процедур, мы получим удобный “язык программирования” для работы с матрицами. Фактически, отличие “программиста” от “не-программиста” сводится к умению такой язык построить.

Каждый раз, когда мы выделяем повторяющуюся последовательность действий в “подпрограмму” – мы повторяем эту операцию. Хороший набор таких подпрограмм – это “библиотека” или “фреймворк”. Учебную задачу из предыдущего пункта вполне можно развить аж до полноценной системы линейной алгебры. От полноценного языка программирования она будет отличаться лишь использованием синтаксиса и управляющих конструкций языка реализации. Но никто не запрещает нам говорить, скажем, о языке “C с линейной алгеброй” – в конце концов, “стандартная библиотека” – это тоже часть языка, и расширяя ее – мы расширяем и язык.

Что же ограничивает программиста, который занят созданием такого языка? Его ограничивает только “расширяемый” язык. Скажем, если мы вдруг вспомним, что матрица – это линейный оператор, и операция называется не “умножить матрицу на вектор”, а “применить оператор к вектору” – то в случае языка типа C или Pascal мы попали, причем конкретно (это не говоря о том, что написать y = Ax мы все равно не можем, а можем писать лишь y = multiply(A, x)). C++ предоставляет новую возможность – определим Matrix::operator*(Vector) и слегка упростим себе жизнь. В более “абстрактных” языках программирования доступны и другие средства манипуляции с объектами из определенного нами языка, в том числе – делающие его действительно частью исходного (это возможно не всегда, приведу чисто “программистский” пример – при всем богатстве std::string в C++ мы не можем написать string s = "foo" + "bar").

Так или иначе, но “главный метод программирования” можно сформулировать в виде (спертом у [info]lionet):

Когда программист пишет модульную программу для решения задачи, он сначала разбивает задачу на подзадачи, затем решает подзадачи, и в конце концов объединяет решения.

…а затем прочитать продолжение:

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

Соответственно, если в проекте возникает необходимость в выделении какого-то “внешнего” по отношению к нему скриптового языка, то можно сделать простой вывод – “основной” язык недостаточно хорош и гибок. В подавляющем большинстве случаев – это ошибка при выборе этого языка (я сразу же исключаю из рассмотрения патологические случаи типа 3D-шутеров со сложными внутренними скриптами – как только в каком-нибудь Lua можно будет обеспечить быстродействие графики на уровне нынешних движков iD Software, Quake N+1 будет написан именно на нем).

Теперь выводы. Вместо того, чтобы заниматься разработкой “скриптового языка”, желательно попытаться описать API проекта – и сделать его доступным для всех желающих. Вариант второй. Если применение чего-то неповоротливого и низкоуровневого неизбежно (допустим, мы пишем Quake), а API хочется реализовать по принципам высокоуровневых языков – то следует разделить “механизм и политику”. При этом вся функциональность должна быть реализована на “удобном” языке. При этом в последнем случае всегда можно дать пользователям возможность менять код, реализующий ее – во всяком случае, при наличии большого числа высококвалифицированных пользователей или же при наличии у последних “крайне специфических” требований, это очень удобно.

Кстати, очень интересно выглядит в свете этого “десятое правило Гринспена” – про то, что каждая достаточно сложная программа на C содержит заново написанную, неспецифицированную, глючную и медленную реализацию половины языка Common Lisp. Замените этот “глючный и медленный” Common Lisp на “настоящий” Lisp, Python, Lua, Javascript – да на все что угодно! – и получите огромное количество удобств. Выдайте пользователю REPL этого языка – и получите полноценный скриптовый язык в вашем приложении “малыми силами”.