Дапамажыце dev.by 🤍
Падтрымаць

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

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

Этот список эзотерических языков программирования с примерами кода определенно способен расширить ваше программерское сознание. А возможно и сподвигнет к изучению чего-нибудь эдакого в свободное от написания очередного энтерпрайз-приложения на 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.

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

Источник
 

Чытайце таксама
Python больш не самы папулярны на GitHub. Яго змяніў вельмі малады язык
Python больш не самы папулярны на GitHub. Яго змяніў вельмі малады язык
Python больш не самы папулярны на GitHub. Яго змяніў вельмі малады язык
7 каментарыяў
У Расіі лакалізавалі JavaScript
У Расіі лакалізавалі JavaScript
У Расіі лакалізавалі JavaScript
7 каментарыяў
JetBrains: кожнаму шостаму распрацоўшчыку ШІ эканоміць мінімум дзень на тыдзень
JetBrains: кожнаму шостаму распрацоўшчыку ШІ эканоміць мінімум дзень на тыдзень
JetBrains: кожнаму шостаму распрацоўшчыку ШІ эканоміць мінімум дзень на тыдзень
1 каментарый
IEEE: з-за ШІ стала немагчыма адсочваць папулярнасць моў, іх будучыня пад пытаннем
IEEE: з-за ШІ стала немагчыма адсочваць папулярнасць моў, іх будучыня пад пытаннем
IEEE: з-за ШІ стала немагчыма адсочваць папулярнасць моў, іх будучыня пад пытаннем

Хочаце паведаміць важную навіну? Пішыце ў Telegram-бот

Галоўныя падзеі і карысныя спасылкі ў нашым Telegram-канале

Абмеркаванне
Каментуйце без абмежаванняў

Рэлацыраваліся? Цяпер вы можаце каментаваць без верыфікацыі акаўнта.

Каментарыяў пакуль няма.