Этот список эзотерических языков программирования с примерами кода определенно способен расширить ваше программерское сознание. А возможно и сподвигнет к изучению чего-нибудь эдакого в свободное от написания очередного энтерпрайз-приложения на Java или .NET.
Мне регулярно приходится сталкиваться с языками программирования, которые работают настолько нетривиально, что приходится просто пересмотреть свои представления о том, что такое «программный код». В этом посте я хотел бы рассказать о самых интересных моих находках такого рода.
Можете быть уверены, это не дедовский ностальгический пост в стиле «функциональное программирование изменит мир», собранные здесь языки гораздо более необычны. Рискну предположить, что многие читатели слыхом не слыхивали о большинстве описанных ниже языков и парадигм. Надеюсь, это знакомство окажется для вас не менее интересным, чем было для меня.
Обратите внимание: я обладаю минимальным опытом работы со всеми нижеперечисленными языками. Идеи, лежащие в их основе, кажутся мне интересными, но я не претендую на экспертное мнение по их поводу. Поэтому прошу указывать любые найденные ошибки и предлагать исправления. Если вы знаете и другие подобные идеи и парадигмы, не рассмотренные здесь – расскажите о них!
Конкурентность по умолчанию
Примеры языков: ANI, Plaid
Итак, давайте сразу начнем с настоящего парадокса: существуют языки программирования, в которых конкурентность применяется по умолчанию. То есть, любая строка кода выполняется параллельно!
Допустим, вы написали три строки кода: A, B и C:
A; B; C;
В большинстве языков программирования сначала будет выполнена строка A, затем B и, наконец, C. Но в языке наподобие ANI все три строки будут выполнены одновременно!
Поток выполнения или упорядочивание строк кода в ANI – всего лишь побочный эффект явных зависимостей, существующих между этими строками. Например, если в B будет ссылка на переменную, определенную в A, то A и C выполнятся одновременно, а B – только после того, как выполнится A.
Рассмотрим пример кода на языке ANI. Как описано в руководстве, программы на ANI состоят из «каналов» (pipe) и «защелок» (latch), используемых для управления аппаратными потоками и потоками данных. Синтаксис необычный и разобрать его непросто, причем язык скорее мертв, чем жив. Но его концепции кажутся интересными.
Вот пример "Hello World" на языке ANI:
"Hello, World!" ->std.out
В терминологии ANI это означает, что мы посылаем объект "Hello, World!" (строку) в аппаратный поток std.out. А что получится, если мы пошлем в std.out еще одну строку?
"Hello, World!" ->std.out "Goodbye, World!" ->std.out
Обе эти строки кода выполняются параллельно, поэтому они могут выйти на консоль в любом порядке. Рассмотрим, что получится, если мы используем в одной строке переменную и позже сошлемся на нее:
s = [string\]; "Hello, World!" ->s; \s ->std.out;
В первой строке объявляется защелка s. Защелки немного напоминают переменные. Данная защелка содержит строку. Вторая строка кода отправляет текст "Hello, World!" в s, третья строка «отщелкивает» s и отсылает ее содержимое в std.out. Здесь мы видим неявное упорядочивание программы на ANI: поскольку каждая строка зависит от предыдущей, код будет выполняться именно в том порядке, в котором он записан.
В языке Plaid также заявлена поддержка конкурентности по умолчанию, но данный язык работает по модели прав доступа, описанной в этой статье. Права доступа позволяют выстроить поток выполнения. В Plaid также исследуются другие интересные концепции, в частности, программирование, ориентированное на состояние типов. В такой парадигме программирования изменение состояния является сущностью первого порядка: объекты определяются не как классы, а как последовательности состояний и переходов, которые могут быть проверены компилятором. Здесь мы наблюдаем интересную попытку представить время в языке программирования как конструкт первого порядка. Эта проблема рассматривается в лекции Рича Хики «Мы уже прибыли?»
В настоящее время бурно развиваются многоядерные технологии, и поддерживать конкурентность на должном уровне во многих языках все сложнее. ANI и Plaid позволяют заново взглянуть на эту проблему, а в перспективе значительно повысить производительность. Правда, остается вопрос о том, усложняется или упрощается управление конкурентностью при «параллелизме по умолчанию».
Зависимые типы
Примеры языков: Idris, Agda, Coq
Вероятно, вам привычны системы типов, существующие, например, в языках C или Java. Типы позволяют компилятору проверять, что представляет из себя переменная – например, содержит ли она целое число, список или строку. А что если бы компилятор также мог проверять, содержит ли переменная «положительное число», «список длиной 2» или «строку-палиндром»?
Именно такие идеи лежат в основе языков, использующих зависимые типы. Можно задать тип, способный проверять значение переменных во время компиляции. В библиотеке shapeless для Scala предоставляется экспериментальная поддержка зависимых типов (читай: пока не вполне доработанная). С ее помощью легко создать примеры использования таких типов.
Вот как можно объявить при помощи этой библиотеки тип Vector, содержащий значения 1, 2 и 3:
val l1 = 1 :#: 2 :#: 3 :#: VNil
Здесь создается переменная l1. Из ее сигнатуры типа понятно, что перед нами Vector, содержащий Ints, а также видно, что это Vector с длиной 3. Компилятор может использовать эту информацию для перехвата ошибок. Давайте задействуем в Vector метод vAdd для выполнения попарного сложения двух Vectors:
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: 3 :#: VNil val l3 = l1 vAdd l2 // Итог: l3 = 2 :#: 4 :#: 6 :#: VNil
Вышеприведенный пример работает нормально, поскольку системе типов известно: оба Vectors имеют длину 3. Но если бы мы попытались сложить при помощи vAdd два Vectors разной длины, то получили бы ошибку во время компиляции, а не во время выполнения!
val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: VNil val l3 = l1 vAdd l2 // Итог: возникает ошибка компиляции, так как невозможно попарно // складывать одномерные массивы, имеющие разную длину
Библиотека Shapeless довольно интересна, но, на мой взгляд, в ней еще хватает шероховатостей. Она поддерживает лишь ограниченное подмножество зависимых типов. Кроме того, создаваемый с ее помощью код и сигнатуры типов получаются довольно пространными. В свою очередь, в языке Idris типы являются первоклассными сущностями, поэтому система зависимых типов представляется гораздо более мощной и аккуратной. Для сравнения можете ознакомиться с лекцией «Scala и Idris: зависимые типы сегодня и завтра».
Существуют методы формальной верификации типа long, но они слишком неудобны для активного использования в повседневном программировании. Зависимые типы в таких языках, как Idris, а в будущем, вероятно, и в Scala, могут предложить более легковесные и практичные альтернативы для значительного повышения эффективности системы типов при отслеживании ошибок. Разумеется, никакая система зависимых типов не сможет перехватить все ошибки в силу неизбежных ограничений, обусловленных проблемой останова. Но при условии умелого применения зависимые типы могут стать новым большим скачком в системах статических типов.
Конкатенативные языки
Примеры языков: Forth, cat, joy
Вы когда-нибудь задумывались, как выглядела бы программа, не содержащая переменных и не использующая функций? Я – нет. Но оказалось, что даже такие идеи обсуждаются всерьез, и их сторонники сформулировали концепцию конкатенативного программирования. Основной смысл этой парадигмы таков: все сущности в языке являются функциями, помещающими информацию в стек. Программы выстраиваются практически исключительно при помощи функциональной композиции (конкатенация – это композиция).
На первый взгляд это кажется слишком абстрактным, так что давайте рассмотрим пример на языке cat:
2 3 +
Здесь мы помещаем в стек два числа, а затем вызываем функцию +, которая извлекает из стека оба числа, суммирует их, а потом записывает в стек результат их сложения. Вывод этого кода равен 5. А вот более интересный пример:
def foo { 10 < [ 0 ] [ 42 ] if } 20 foo
Рассмотрим этот код построчно.
- Сначала мы объявляем функцию foo. Обратите внимание: в языке cat для функций не указывается никаких параметров ввода, все параметры неявно считываются из стека.
- foo вызывает функцию <, которая выводит на верхнюю позицию в стеке первый элемент, сравнивает его с 10, после чего возвращает в стек True или False.
- Далее мы записываем в стек значения 0 и 42. Мы заключаем их в квадратные скобки – так гарантируется, что они попадут в стек невычисленными. Дело в том, что при вызове функции if в следующей строке они будут использоваться как ветви then и else соответственно.
- Функция if извлекает из стека три элемента: булев условный оператор, ветвь then и ветвь else. В зависимости от значения булева условного оператора, результат ветви then или else будет записан обратно в стек.
- Наконец, мы записываем в стек число 20 и вызываем функцию foo.
- Когда все это будет сделано, на выходе имеем число 42.
Гораздо более подробное описание этой парадигмы дается в статье «Красота конкатенативных языков».
Данный стиль программирования обладает некоторыми интересными свойствами. Программы можно дробить и объединять всевозможными способами, создавая таким образом новые программы. Обратите внимание на минимальный синтаксис (даже лаконичнее, чем в LISP), позволяющий писать очень компактные программы. Здесь хорошо поддерживается метапрограммирование. Конкатенативное программирование стало для меня важнейшим мысленным экспериментом, открывшим глаза на многие вещи, но я не уверен в его практичности. Складывается впечатление, что эта парадигма требует от вас запоминать и представлять себе актуальное состояние стека, но не позволяет считывать его по именам переменных в коде. Соответственно, судить о коде может быть достаточно нелегко.
Декларативное программирование
Языки: Prolog, SQL
Декларативное программирование существует уже давно, но многие программисты по-прежнему не понимают его сути. Вот она: в большинстве мейнстримовых языков программирования вы описываете, как решить проблему. В декларативных языках требуется описать только желаемый результат, а сам язык определяет, как его достичь.
Например, если вы создаете с нуля на языке C алгоритм сортировки, то можете написать инструкции для сортировки слиянием (merge sort). Они пошагово описывают, как рекурсивно разбить множество данных пололам, а потом вновь их объединить, но уже в отсортированном виде. Вот пример. Если бы вы сортировали числа в декларативном языке, например, в Prolog, вы просто описываете вывод, который вам требуется. «Мне нужен такой же список значений, но элемент с индексом i должен быть меньше или равен элементу с индексом i + 1». Сравним вышеприведенное решение на C со следующим кодом Prolog:
sort_list(Input, Output) :- permutation(Input, Output), check_order(Output). check_order([]). check_order([Head]). check_order([First, Second | Tail]) :- First =< Second, check_order([Second | Tail]).
Если вы привыкли работать с SQL, то уже имеете опыт декларативного программирования, хотя, возможно, даже не подозревали об этом. При создании запроса вида select X from Y where Z, вы, фактически, описываете множество данных, которые хотите получить в результате. Движок базы данных уже сам определяет, как выполнить такой запрос. В большинстве баз данных можно воспользоваться командой EXPLAIN, чтобы просмотреть план выполнения и понять, как организуется решение задачи на внутрисистемном уровне.
Красота декларативных языков заключается в том, что они позволяют вам работать с гораздо более высокоуровневыми абстракциями. Ваша задача сводится к подробному описанию желаемого вывода. Ниже приведен код простой программы на языке Prolog для решения судоку. Здесь описано, как должны выглядеть каждая строка, столбец и диагональ в решенной задаче судоку:
sudoku(Puzzle, Solution) :- Solution = Puzzle, Puzzle = [S11, S12, S13, S14, S21, S22, S23, S24, S31, S32, S33, S34, S41, S42, S43, S44], fd_domain(Solution, 1, 4), Row1 = [S11, S12, S13, S14], Row2 = [S21, S22, S23, S24], Row3 = [S31, S32, S33, S34], Row4 = [S41, S42, S43, S44], Col1 = [S11, S21, S31, S41], Col2 = [S12, S22, S32, S42], Col3 = [S13, S23, S33, S43], Col4 = [S14, S24, S34, S44], Square1 = [S11, S12, S21, S22], Square2 = [S13, S14, S23, S24], Square3 = [S31, S32, S41, S42], Square4 = [S33, S34, S43, S44], valid([Row1, Row2, Row3, Row4, Col1, Col2, Col3, Col4, Square1, Square2, Square3, Square4]). valid([]). valid([Head | Tail]) :- fd_all_different(Head), valid(Tail).
Вот что получается при запуске этой программы:
| ?- sudoku([_, _, 2, 3, _, _, _, _, _, _, _, _, 3, 4, _, _], Solution). S = [4,1,2,3,2,3,4,1,1,2,3,4,3,4,1,2]
К сожалению, у декларативных языков программирования есть серьезный недостаток: при их использовании программа сильно пробуксовывает в узких местах, связанных с производительностью. Упрощенный алгоритм сортировки, приведенный выше, вероятно, даст O(n!);. Вышеприведенная программа для решения судоку выполняет поиск методом грубой силы. Большинству разработчиков в данном случае пришлось бы запрограммировать подсказки и дополнительные индексы базы данных, чтобы исключить затратные и неэффективные планы при выполнении SQL-запросов.
Символическое программирование
Примеры языков: Aurora
Язык Aurora предназначен для символического программирования. Код, который вы пишете на этом языке, может включать не только обычный текст, но и изображения, математические тождества, графики, диаграммы и т.п. На таком языке вы можете оперировать самыми разнообразными данными, а не излагать всю программу в текстовой форме. Кроме того, язык Aurora полностью интерактивен, мгновенно показывает вам результаты выполнения каждой строки кода – как прокачанный REPL. на стероидах.
Автором языка Aurora является Крис Грейнджер, также написавший интегрированную среду разработки Light Table. Крис признается, что к созданию языка Aurora его подтолкнуло стремление сделать программирование более прозрачным, прямым, а также исключить случайные усложнения. Эти вопросы подробнее рассмотрены в замечательных лекциях Брета Виктора: Inventing on Principle, Media for Thinking the Unthinkable и Learnable Programming.
Апдейт: язык Aurora сложно назвать типичным представителем «символического программирования». Эта парадигма программирования подробнее описана в следующем источнике.
Программирование на основе знаний
Примеры: Wolfram
Язык Wolfram во многом напоминает упомянутый выше язык Aurora, в основе Wolfram также лежит символическое программирование. Однако символический уровень в нем просто предоставляет согласованный интерфейс для взаимодействия с ядром языка Wolfram, которое реализует принципы «программирования на основе знаний». В составе языка присутствует огромный массив библиотек, алгоритмов и данных. На нем легко решать самые разнообразные задачи: строить граф контактов в Facebook, работать с изображениями, проверять прогноз погоды, обрабатывать запросы на естественном языке, наносить маршруты на карте, решать математические уравнения и др.
Полагаю, язык Wolfram обладает наиболее обширной «стандартной библиотекой» и множеством данных из всех существующих сегодня языков программирования. Мне также очень нравится, что в этом языке соединение с интернетом считается базовой составляющей написания кода. Wolfram можно сравнить с интегрированной средой разработки, где функция автозаполнения объединена с поиском в Google. Было бы очень интересно проверить, действительно ли модель символического программирования обладает такой гибкостью, как считается в Wolfram, и позволяет ли она эффективно использовать все эти данные.
Апдейт: хотя в документации заявлено, что Wolfram поддерживает «символическое программирование» и «программирование на основе знаний», два этих понятия не являются полными синонимами. Подробнее о них можно прочитать в следующих статьях: Knowledge level и Symbolic Programming.
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.