Pull to refresh
105.22
Productivity Inside
Для старательного нет ничего невозможного

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

Reading time 9 min
Views 54K
Original author: Yevgeniy Brikman
Периодически я натыкаюсь на языки программирования, которые настолько самобытны, что меняют моё представление о коде в целом. В этой статье я хотел бы поделиться некоторыми из самых любимых моих находок.

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

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



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



Примеры языков: ANI, Plaid

Давайте начнём с того, что по-настоящему потрясает воображение: существуют такие языки, в которых по умолчанию предполагается конкурентность. То есть все строки кода выполняются параллельно друг другу!

Например, представьте, что вы написали три строки кода — А, В и С.

A;
B;
C;

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

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

Давайте разберём пример из ANI. Как говорится в туториале, программы, написанные на ANI, состоят из каналов (pipes) и задвижек (latches), с помощью которых происходит управление потоками данных. В этом необычном синтаксисе не так просто разобраться, а сам язык кажется мёртвым, но концепты он предлагает весьма интересные.

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

Язык Plaid также, согласно заверениям создателей, поддерживает конкурентность по умолчанию, однако использует при этом модель разрешений (подробнее в этой статье) для настройки контроля потока. Plaid также экспериментирует и с другими интересными концепциями, например, программирование, ориентированное на состояние типа, где на первый план выходят изменения состояния: объекты определяются не как классы, а как серии состояний и переходов, которые может отслеживать компилятор. Этот подход представляется мне занятным — время в нём определяется как языковой конструкт первого класса. Rich Hickey писал об этом в своей статье Are we there yet.

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

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



Примеры языков: Idris, Agda, Coq

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

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

Вот как объявить Vector, который содержит значения 1, 2, 3 в библиотеке shapeless:

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

Таким образом, создаётся переменная 11, в сигнатуре которой указано не только то, что это Vector, который содержит Ints, но и то, что длина вектора — 3. Компилятор может использовать эту информацию, что отлавливать ошибки. Давайте применим метод vAdd к вектору, чтобы произвести попарное сложение двух векторов:

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 к векторам разной длины, то получили бы ошибку уже на стадии компиляции, ещё до выполнения!

val l1 = 1 :#: 2 :#: 3 :#: VNil
val l2 = 1 :#: 2 :#: VNil
 
val l3 = l1 vAdd l2
 
// Result: a *compile* error because you can't pairwise add vectors 
// of different lengths!

Shapeless — отличная библиотека, но, насколько мне известно, ещё немного сыроватая: поддерживает только небольшой набор зависимых типов, даёт тяжеловесный код и сигнатуры. Idris же делает типы объектом первого класса в языке программирования, вследствие чего система зависимых типов получается аккуратнее и мощнее. Для детального сравнения почитайте Scala vs Idris: Dependent Types, Now and in the Future.

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

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



Примеры языков: Forth, cat, joy

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

Звучит как-то туманно, поэтому давайте возьмём простой пример из cat:

2 3 +

Здесь мы добавляем в стек два числа, а затем вызываем функцию +, которая выбрасывает оба из стека и добавляет туда их сумму. The output этого кода — 5. Теперь рассмотрим пример чуть поинтереснее:

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

Разберём каждую строку:

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

За более подробным введением вы можете обратиться к The Joy of Concatenative Languages.

Этот стиль программирования отличается некоторыми интересными свойствами: программы можно расщеплять и объединять бесчисленными способами, чтобы создавать новые программы. Стоит также отметить сжатый синтаксис (еще более сжатый, чем у LISP), который делает программы крайне лаконичными и мощную поддержку метапрограммирования. На мой взгляд, конкатенативное программирование даёт много пищи для размышлений, но вот практичность его вызывает сомнения. Представляется, что при таком подходе нужно постоянно помнить или представлять себе текущее состояние стека, вместо того чтобы восстанавливать его по названиям переменных в коде, вследствие чего становится сложнее принимать решения

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



Примеры языков: Prolog, SQL

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

Например, если вы с нуля создаете алгоритм сортировки на С, то вам нужно прописать инструкции для сортировки слиянием, в которых будет пошагово расписано, как рекурсивно разделить все данные на две части, а потом объединить в соответствующем сортировке порядке; вот пример. Если вы сортируете числа в декларативном языке вроде Prolog, вместо всего этого вы прописываете желаемый результат: «Я хочу получить список из тех же составляющих, но число на позиции i должно быть меньше или равно числу на позиции i+1». Сравните решение на С, о котором шла речь выше, с этим кодом из 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 был создан Chris Granger, авторству которого принадлежит также Light Table IDE. Он говорит о мотивации, которая побудила его изобрести Aurora, в своём посте Toward a better programming. Он преследовал такие цели, как сделать программирование более наглядным, прямым и свести случайное усложнение к минимуму. Если хотите узнать больше, обязательно ознакомьтесь с материалами от Bret VictorInventing on Principle, Media for Thinking the Unthinkable и Learnable Programming.

Программирование, основанное на знаниях



Пример языка: Wolfram

Как и описанный выше Aurora, язык Wolfram также основывается на принципах символического программирования. Но символический слой — это всего лишь способ обеспечить стабильный интерфейс для того, что составляет ядро — то есть программирование, основанное на знаниях, широкий круг встроенных в язык библиотек, алгоритмов и данных. Благодаря такому подходу, можно легко и просто делать что угодно: строить графики на базе статистики по вашем аккаунту на Facebook, редактировать картинки, смотреть погоду, обрабатывать запросы на естественном языке, прокладывать маршруты на карте, решать уравнения и так далее.

Я полагаю, что из всех существующих языков у Wolfram самая обширная «стандартная библиотека» и набор данных. Также меня очень вдохновляет мысль, что связь с Интернет-сообществом становится неотъемлемой частью процесса написания кода: это напоминает IDE, где функция автозаполнения осуществляет поиск в Google. Интересно будет проверить, правда ли символическая модель программирования настолько гибкий, как утверждает Wolfram, и может ли она должным образом воспользоваться всеми этими данными.
Tags:
Hubs:
+32
Comments 49
Comments Comments 49

Articles

Information

Website
productivityinside.com
Registered
Founded
Employees
101–200 employees
Location
Россия