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

У [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 этого языка — и получите полноценный скриптовый язык в вашем приложении «малыми силами».

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

  1. > как только в каком-нибудь Lua

    Нет. Скорее Haskell/ML. У Lua динамическая типизация, поэтому мы сразу же теряем массу проверок. Я, честно говоря, вообще не понимаю, зачем скрипты делают на Lua.

    1. Это не принципиально. Хотя Haskell или ML для «скриптового языка» выглядят слишком сложными. Тут же еще один момент, который я не освещаю — скриптовый язык не может быть сложным. Если в «элементарном» руководстве речь идет про монады и систему типов Хиндли-Милнера — то в топку такой язык. Это означает лишь то, что соответствующие абстракции настолько сложны или непонятны, что о них нельзя рассказать так же элементарно, как об абстракциях структурного или процедурного программирования.

      1. > Это не принципиально. Хотя Haskell или ML для “скриптового языка” выглядят слишком сложными.

        Я бы не сказал. Вы, к примеру, Шура, знаете, что весь UNIX стоит на зачаточных ленивых вычислениях и функциональщине? :-) Т.е. конвееры и обратные кавычки.

        А строгая статическая типизация в ML’евском варианте ничуть не сложнее динамической, только выявление ошибок сильно проще.

        1. Вот дело как раз в том, во что эти «зачаточные ленивые вычисления и функциональцина» завернуты. Конвейеры и кавычки использовать гораздо проще, чем «настоящую» функциональщину в виде лямбда-исчисления. Можно даже об этом не задумываться и не ощущать себя ущербным.

          1. > Вот дело как раз в том, во что эти “зачаточные ленивые вычисления и функциональцина” завернуты. Конвейеры и кавычки использовать гораздо проще, чем “настоящую” функциональщину в виде лямбда-исчисления. Можно даже об этом не задумываться и не ощущать себя ущербным.

            Конечно. Более того, о «ленивости» bash’а даже профессионалы не всегда задумываются. Это и есть «высший класс» — минимум слов, максимум дела и простота.

            Но вот возникает интересная задача, поставленная Куздрой — как всандалить статическую типизацию вроде ML’ной в командную строку UNIX. Чтобы и просто, и удобно.

            Я не очень понимаю, как это сделать, но если сделать, то это, похоже, и есть Unix#2, о котором так долго говорил Завалишин. Или нет?

          2. > Конвейеры и кавычки использовать гораздо проще, чем “настоящую” функциональщину в виде лямбда-исчисления.

            Ленивость, скажем прямо, вы в синтаксисе и не увидите. А «лямбды» — ну, господи, функции без имени. Сравните:

            cat ‘find . -name *.txt’ | sort

            и

            List.sort (find . -name *.txt)

            Ведь для UNIX скрипта файл — это переменная с именем /home/rdia/u.txt и типом string == char list.

            1. > А “лямбды” – ну, господи, функции без имени

              При попытке объяснить, что такое «лямбды», были упомянуты две очень мощные абстракции — функции и имена объектов (упоминание Бога, как очень-очень крутой абстракции, так и быть, оставлю без внимания). А ведь «лямбды» в программировании к лямбда-исчислению Черча имеют весьма опосредованное отношение :)

              1. > При попытке объяснить, что такое “лямбды”, были упомянуты две очень мощные абстракции – функции и имена объектов

                Я не вижу тут особых проблем. В unix’ах ведь тоже приходится объяснять разное. Более того, в bash’е вы должны объяснить, что конвееры — это ленивые вычисления, а кавычки — энергичные.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *