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

6494

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

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

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

Примечание. Я в восторге от некоторых идей, но пока не опробовал их на практике. Поэтому смело указывайте, где и что нужно исправить. Также не стесняйтесь делиться новыми идеями, если знаете, и не нашли в этом очерке.

Многопоточность по умолчанию

 

Anic
Anic

Языки: ANIPlaid

А вот и первая головоломка: есть языки программирования, которые являются многопоточными по умолчанию. То есть, каждая строка кода выполняется параллельно!

Например, представьте, что вы написали три строки кода, A, B и C:

A;
B;
C;

В большинстве языков программирования, сначала выполняется A, затем B, а потом C. На таком языке, как ANI, A, B и C будут выполняться одновременно!

Управление потоком или упорядочивание строк в ANI является лишь побочным эффектом явных зависимостей между строками кода. Например, если бы В имела ссылку на переменную, определенную в А, тогда А и С выполнялись бы одновременно, а В — только после завершения А.

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

Пример команды «Hello World» в Ани:

"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, содержащую тип данных; вторая строка отправляет в s текст «Hello, World!»; третья строка «открывает задвижку» s и отправляет содержимое в std.out. Можно увидеть неявную последовательность программы в ANI: поскольку каждая строка зависит от предыдущей, код будет выполняться в том порядке, как он написан.

Plaid также представлен, как язык, поддерживающий многопоточность по умолчанию, но использует модель разрешений для настройки управления потоком, как описано в этом документе. У этого языка есть не менее интересные концепции, такие как программирование, ориентированное на состояние типов, где изменения состояния становятся объектами первого класса. В этом случае вы определяете объекты не как классы, а последовательность состояний и этапов, которые проверяет компилятор. Похоже на интересный подход в представлении времени в качестве конструкции языка первого класса, как обсуждалось Ричем Хикки в его речи Мы все еще здесь.

В последнее время стремительно растет популярность многоядерных решений, а вот с многопоточностью дела обстоят намного хуже, чем этого хотелось бы, причем во многих языках. Так вот с помощью языков ANI и Plaid программисты предлагают новое решение, которое может привести к колоссальному приросту производительности. Вопрос лишь — облегчит ли «параллельность по умолчанию» управление многопоточностью.

Уточнение. Описание, которое приводится выше, объясняет основные принципы языков ANI и Plaid. Я использую термины «многопоточный» и «параллельный» как синонимы, хотя у них разные значения. За дополнительной информацией смотрите «Многопоточность это не параллелизм».

Зависимые типы

Язык Idris
Язык Idris

Языки: IdrisAgdaCoq

В языках С и Java всегда использовались системы типов, где компилятор проверяет, является ли переменная целым числом, списком или строкой. Но что, если бы компилятор мог проверить, является ли переменная «положительным числом», «списком длиной 2» или «строкой-палиндромом»?

В этом и состоит замысел языков с поддержкой зависимых типов: можно определить типы, которые проверяют значение переменных в процессе компиляции. Библиотека shapeless в языке Scala обеспечивает частичную экспериментальную поддержку () для зависимых типов, что видно на простых примерах.

Способ, как можно объявить элемент Vector, содержащий значения 1, 2, 3 с помощью библиотеки shapeless:

val l1 = 1 :#: 2 :#: 3 :#: VNil

Создается переменная l1, тип которой определяет не только то, что это Vector, содержащий Ints, но и Vector длины 3. Компилятор может использовать эту информацию для обнаружения ошибок. Испытаем метод vAdd в функции Vector для парного сложения векторов:

val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: 3 :#: VNil
 
val l3 = l1 vAdd l2
 
// Result: l3 = 2 :#: 4 :#: 6 :#: VNil

Приведенный выше пример прекрасно работает, потому что система типов знает, что оба вектора имеют длину 3. Однако если мы попробуем сложить с помощью команды vAdd два вектора разной длины, то, вместо ожидания времени выполнения получим ошибку compile time.

val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: VNil
 
val l3 = l1 vAdd l2
 
// Результат: ошибка *compile* из-за невозможности парного сложения векторов 
// различной длины!

Shapeless — великолепная библиотека. Но как видим из этого примера, она немного сыровата, так как поддерживает только подклассы зависимых типов и требует составления подробного кода и сигнатур. В языке Idris типы являются объектами первого класса, и системы типов более функциональны и кратки. Для сравнения можно послушать лекцию Scala и Idris: Зависимые типы сейчас и в будущем.

Методы формальной верификации используются уже долгое время, но часто являются слишком громоздкими для универсального программирования. Зависимые типы в таких языках, как Idris или даже Scala в будущем, возможно, произведут легковесные альтернативы и увеличат способность системы типов при обнаружении ошибок. Конечно, ни одна система типов не может обнаружить все ошибки из-за ограничений, унаследованных от проблемы остановок. Но при хорошем ее выполнении зависимые типы могут стать следующим большим шагом к статическим системам типов.

Конкатенативный язык программирования

Конкатенативный язык программирования
Конкатенативный язык программирования

Языки: Forthcatjoy

Когда-нибудь задумывались над тем, как это программировать без переменных и функций? Нет? Я тоже. Видимо, другие поразмышляли и в результате придумали конкатенативное программирование. Идея заключается в том, что все в языке является функцией, которая отправляет данные в стек либо извлекает данные из стека. Программы строятся почти исключительно из композиции функций (конкатенация — это композиция).

Это звучит довольно абстрактно. Лучше рассмотрим простой пример в cat:

2 3 +

Здесь мы отправляем два числа в стек, а затем вызываем функцию +, которая выводит оба числа из стека и отправляет результат их сложения обратно в стек. Вывод кода составляет 5. Вот пример поинтереснее:

def foo {
  10 <
  [ 0 ]
  [ 42 ]
  if
}
 
20
foo

Рассмотрим каждую строку:

  1. Сначала мы объявляем функцию foo. Заметьте, что функции в cat определяются без входных параметров. Все параметры неявно считываются из стека.
  2. foo вызывает функцию <, которая извлекает из стека первый элемент, сравнивает его с 10 и помещает True или False обратно в стек.
  3. Далее мы отправляем в стек значения 0 и 42; заключаем их в скобки, чтобы поместить их в стек без вычисления. Делаем это так, поскольку числа будут использоваться в качестве ветвей “then” и “else” для вызова функции if в следующей строке.
  4. Функция if извлекает из стека 3 элемента: логическое условие, ветвь «then» и ветвь «else«. В зависимости от значения логического условия результат из ветвей “then” или “else” будет направлен назад в стек.
  5. Наконец, отправляем число в стек число 20 и вызываем функцию foo.
  6. После выполнения всего сказанного получаем в результате число 42.

За подробными разъяснениями смотрите статью Великолепие Конкатенативных языков.

Этот стиль программирования имеет несколько интересных свойств: программы можно разделить и объединить различными способами для создания новых программ, а также минимум кода (даже меньше, чем Lisp) и полная поддержка метапрограммирования. Конкатенативное программирование — поразительный мысленный эксперимент, но относительно его практичности есть сомнения. Складывается впечатление, что нужно помнить или представлять текущее состояние стека, не имея возможности прочитать их по именам переменных в коде, что создает трудности в понимании кода.

Декларативное программирование

Декларативное программирование
Декларативное программирование GNU Prolog

Например языки: PrologSQL

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

Например, вам нужно с нуля составить алгоритм сортировки в C. Тогда вы пишете инструкции для сортировки слиянием, в которой шаг за шагом указано, как рекурсивно разделить набор данных пополам и объединить их вместе в отсортированном порядке. Здесь пример. Если вы сортируете числа в декларативном языке, например в 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 IDE. Мотивацию к созданию языка Aurora Крис изложил в посте В поисках лучшего программирования. Некоторые цели: сделать программирование более доступными, простым и сократить вероятность непредвиденных сложностей. Чтобы все это понять, лучше посмотреть замечательные доклады Брета ВиктораИзобретать по принципуМедиа для размышления о немыслимом и Программированию можно научиться.

Уточнение. «Графо-символическое программирование», вероятно, не является правильным термином Aurora. Смотрите статью в Графосимволическое программирование в Википедии.

Программирование на базе знаний

Программирование на базе знаний
Программирование на базе знаний

Для примера: Wolfram Language

Языки: Wolfram Language

Так же, как упомянутый выше язык Aurora, Wolfram Language основывается на символическом программировании. Тем не менее символический слой — это просто способ обеспечить последовательный интерфейс к ядру Wolfram, который является языком на основе баз знаний — огромного массива библиотек, алгоритмов и данных. С помощью языка можно делать все: отображать графически подключения на Facebook, обрабатывать изображения, просматривать прогноз погоды, обрабатывать запросы на естественном языке, составлять маршруты на карте, решать математические уравнения и многое другое.

Кажется, язык Wolfram имеет наибольшую «стандартную библиотеку» и набор данных. Кроме того, неотъемлемой частью написания кода является подключение к интернету — практически среда разработки, где функция автозаполнения выполняет поиск Google. Интересно увидеть — будет ли графосимволическое программирование таким же универсальным, как Wolfram.

Уточнение. Хотя говорится, что Wolfram Language поддерживает графо- символическое программирование и программирование на основе базы данных, у этих понятий немного разные определения. Читайте статьи в Википедии Уровень знаний и Символическое программирование.