Шесть парадигм программирования, которые изменят ваше представление о коде

14 апреля 2014, 11:06

Этот список эзотерических языков программирования с примерами кода определенно способен расширить ваше программерское сознание. А возможно и сподвигнет к изучению чего-нибудь эдакого в свободное от написания очередного энтерпрайз-приложения на Java или .NET.

Мне регулярно приходится сталкиваться с языками программирования, которые работают настолько нетривиально, что приходится просто пересмотреть свои представления о том, что такое «программный код». В этом посте я хотел бы рассказать о самых интересных моих находках такого рода.

Можете быть уверены, это не дедовский ностальгический пост в стиле «функциональное программирование изменит мир», собранные здесь языки гораздо более необычны. Рискну предположить, что многие читатели слыхом не слыхивали о большинстве описанных ниже языков и парадигм. Надеюсь, это знакомство окажется для вас не менее интересным, чем было для меня.

От cat и joy до Wolfram

Обратите внимание: я обладаю минимальным опытом работы со всеми нижеперечисленными языками. Идеи, лежащие в их основе, кажутся мне интересными, но я не претендую на экспертное мнение по их поводу. Поэтому прошу указывать любые найденные ошибки и предлагать исправления. Если вы знаете и другие подобные идеи и парадигмы, не рассмотренные здесь – расскажите о них!

Конкурентность по умолчанию

Примеры языков: 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

Рассмотрим этот код построчно.

  1. Сначала мы объявляем функцию foo. Обратите внимание: в языке cat для функций не указывается никаких параметров ввода, все параметры неявно считываются из стека.
  2. foo вызывает функцию <, которая выводит на верхнюю позицию в стеке первый элемент, сравнивает его с 10, после чего возвращает в стек True или False.
  3. Далее мы записываем в стек значения 0 и 42. Мы заключаем их в квадратные скобки – так гарантируется, что они попадут в стек невычисленными. Дело в том, что при вызове функции if в следующей строке они будут использоваться как ветви then и else соответственно.
  4. Функция if извлекает из стека три элемента: булев условный оператор, ветвь then и ветвь else. В зависимости от значения булева условного оператора, результат ветви then или else будет записан обратно в стек.
  5. Наконец, мы записываем в стек число 20 и вызываем функцию foo.
  6. Когда все это будет сделано, на выходе имеем число 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.

Евгений Брикман

Источник
 

подписка на главные новости 
недели != спам
# ит-новости
# анонсы событий
# вакансии
Обсуждение