вНовости программирования

Парадигмы программирования, которые изменят ваше отношение к кодингу

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

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

парадигмы программирования

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

Допустим, у нас есть три строки кода:

A; B; C;

При последовательном программировании эти строки выполнялись бы по очереди: сначала A, потом B, потом C. Такой принцип по умолчанию реализован в большинстве языков, и мы привыкли проектировать, держа его в голове. Но в языке ANI все три строки являются конкурентными, то есть могут выполняться в пересекающихся промежутках времени, в том числе и параллельно.

Примечание Термины «конкурентность» и «параллелизм» часто путают и пытаются отождествить, что объяснимо ввиду тесной связи между понятиями, но это в корне неверно. Статья про параллелизм в Википедии содержит следующее пояснение:

В русскоязычной литературе нередко путаются термины «параллелизм» и «конкурентность». Оба термина означают одновременность процессов, но первый — на физическом уровне (параллельное исполнение нескольких процессов, нацеленное только на повышение скорости исполнения за счёт использования соответствующей аппаратной поддержки), а второй — на логическом (парадигма проектирования систем, идентифицирующая процессы как независимые, что в том числе позволяет их исполнять физически параллельно, но в первую очередь нацелено на упрощение написания многопоточных программ и повышение их устойчивости).

Окончательно разобраться в терминологии поможет наша статья про конкурентность и видео «Concurrency Is Not Parallelism»:

Но вернемся к ANI, который реализует конкурентность по умолчанию. Порядок выполнения в данном случае является побочным эффектом явно заданных зависимостей между строками кода: если B содержит ссылку на переменную, определенную в A, то A и C могут выполняться в любом порядке, а B — только после того, как завершится выполнение A.

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

Вот привет миру:

"Hello, World!" ->std.out

В терминологии ANI, мы отправляем объект "Hello, World!" (строку) в поток 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. Так как каждая строка зависит от предыдущей, программа выполнится в том же порядке, в котором она написана.

Язык Plaid также поддерживает конкурентность по умолчанию, но использует систему ограничений для контроля за порядком выполнения (подробнее в статье). Кроме того, Plaid реализует парадигму Typestate-Oriented Programming, которая является своеобразным расширением ООП: объекты определяются не классами, а последовательностью состояний и переходов, которые могут проверяться компилятором. Это интересный подход к представлению времени в языке как объекта первого класса. Такая парадигма была подробно описана в презентации Are We There Yet? создателем Clojure Ричем Хикки (Rich Hickey).

В целом и конкурентность, и параллелизм — уже давно привлекательные и активно развивающиеся концепции: их эффективная реализация ведёт к сильному повышению производительности. Только задача эта сложная, а потому интересно, может ли конкурентность её упростить (хотя бы в теории).

Список языков, так или иначе поддерживающих конкурентные вычисления, можно найти в соответствующей вики-статье. На картинке, предваряющей текст, — проблема обедающих философов, про нее тоже интересно почитать.

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

парадигмы программирования

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

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

Этот вопрос приводит к рассмотрению языков программирования, в которых реализована поддержка зависимых типов, то есть возможность определять типы, проверяющие значения переменных на стадии компиляции. За примером можно обратиться к Scala и подключаемой библиотеке shapeless, которая из экспериментального проекта за несколько лет выросла до относительно стабильного инструмента.

Объявим структуру Vector, содержащую значения 1, 2 и 3:

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

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

Используем метод vAdd для попарного сложения значений:

val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: 3 :#: VNil val l3 = l1 vAdd l2 // Результат: l3 = 2 :#: 4 :#: 6 :#: VNil

Код выше работает, потому что система типов знает, что обе структуры имеют одинаковую длину, равную 3. Но если мы попытаемся применить метод vAdd к структурам разной длины, ошибка обнаружится до выполнения, то есть на этапе компиляции:

val l1 = 1 :#: 2 :#: 3 :#: VNil val l2 = 1 :#: 2 :#: VNil val l3 = l1 vAdd l2 // Результат: ошибка компиляции

Библиотека shapeless довольно привлекательна, но существуют гораздо более мощные решения, например, язык Idris, в котором типы являются объектами первого класса, и, соответственно, система зависимых типов реализована элегантнее и чище. Ради интереса можете посмотреть сравнительную презентацию Scala vs Idris: Dependent Types, Now and in the Future.

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

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

парадигмы программирования

Примеры языков: Forth, Kitten, Joy, Cat

Конкатенативные или стековые языки программирования основаны на том, что конкатенация («склеивание») двух фрагментов кода выражает их композицию (применение одной функции к результату другой). Эти языки используют стек для хранения аргументов и возвращаемых значений операций.

Звучит довольно абстрактно, давайте разберём на конкретных примерах из языка Cat (он считается архивным, но это никак не влияет на его способность иллюстрировать общую идею рассматриваемого семейства языков):

2 3 +

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

Посмотрим на пример поинтереснее, реализующий условную конструкцию:

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

Что здесь происходит? По строкам:

  1. Мы объявляем функцию foo. Заметьте, что у функций в Cat не указываются параметры — все они считываются из стека.
  2. foo вызывает функцию <, которая берет первое число из стека, сравнивает его с числом 10 и добавляет в стек результат — True или False.
  3. Добавляем в стек значение 0
  4. …и значение 42. Скобки нужны для изоляции чисел: они будут использоваться в качестве ветвей «then» и «else» соответственно.
  5. Функция if берет три элемента из стека: булевый тип и две ветви условной конструкции. В зависимости от булевого значения, она добавит в стек число, записанное в одной из ветвей.
  6. Добавляем в стек число 20.
  7. Вызываем foo.

Программа выведет число 42. Более детальное и расширенное объяснение можно посмотреть в статье The Joy of Concatenative Languages.

Еще мы в апреле нашли на GitHub стековый калькулятор Clac, с ним тоже стоит ознакомиться, если заинтересовала тема. Работает он вот так:

clac

У такого стиля программирования есть интересные качества:

  • программы можно «делить» и «склеивать» бессчетным количеством способов для создания новых программ;
  • примечательный минимализм синтаксиса, обеспечивающий лаконичность кода;
  • сильно развитая поддержка метапрограммирования.

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

Подробнее разобраться с принципами работы стека можно с помощью наших статей: «Алгоритмы и структуры данных для начинающих: стеки и очереди» и  «Основные принципы программирования: стек и куча».

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

парадигмы программирования

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

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

Допустим, если мы с нуля пишем алгоритм для сортировки слиянием на языке 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, то вы, возможно не осознавая этого, применяли декларативный подход: при формировании запроса типа выбрать X из Y где 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!), а наш решатель судоку использует брутфорс при поиске. И вообще, в большинстве случаев при составлении запросов к БД приходится применять дополнительные подсказки и указатели для того, чтобы избежать дорогостоящих и неэффективных планов выполнения.

Визуальное программирование

парадигмы программирования

Примеры языков: Pure Data, Grasshopper 3D

Визуальные языки программирования (VPL) позволяют использовать в разработке графические элементы вместо текстовых команд. Такие элементы, будучи систематически упорядоченными и составляющими основной контекст языка, играют роль ввода и/или вывода, процессов и связей внутри программы.

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

Например, язык Pure Data относится к обеим этим парадигмам. Он был разработан в 1990-е годы для создания интерактивных компьютерных музыкальных и мультимедийных произведений.

Следует различать визуальные языки программирования и визуальные средства разработки — последние имеют стандартный текстовый контекст языка, просто он спрятан под GUI, который сильно упрощает создание того или иного продукта. Тем не менее, это также интересное развивающееся направление, которое находит свое применение, например, в проектировании устройств из области Интернета вещей.

Одним из таких инструментов, представленных в соответствующем обзоре, является Modkit — среда разработки под платформу Arduino:

Еще один яркий пример — это Blueprints Visual Scripting для Unreal Engine, скриптовый язык, позволяющий дизайнерам проектировать игры на уровне, обычно доступном только программистам.

Наукоемкое программирование

парадигмы программирования

Примеры языков: Wolfram Language

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

Это делает его подходящим для решения большого количества сложных и узкоспециализированных задач — он позволяет обрабатывать запросы на естественном языке, решать математические уравнения, работать с изображениями, строить графы и делать многое, многое другое. Предположительно, у Wolfram самые большие стандартная библиотека и база данных из всех существующих языков программирования.

Эзотерические языки программирования

парадигмы программирования

Примеры языков: Brainfuck, FALSE

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

Чтобы понять, насколько эзотерические языки зачастую отличаются от классических, достаточно посмотреть на несколько примеров программ Hello, World!:

  • Brainfuck — один из самых известных языков программирования такого рода, спровоцировавший создание целой группы других языков. За некоторым исключением, все символы кроме ><+-.,[] считаются за комментарии и игнорируются:
++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>.
  • Brainloller — клон Brainfuck, в котором команды считываются с пикселей изображения в формате PNG:

парадигмы программирования

  • Pi — основан на Brainfuck. Он вычисляет знаки числа π, при этом периодически совершая ошибки и таким образом кодируя запутанные Brainfuck-инструкции:
3.1415926535897932384226433232725028841271693992751358239749245923072164062 822089986780348053431120629821483865332223366470908446395535832317223594083 284831172502841037019385311052596446029489529303219642288009756259134461214 751482337817834652812009021456285664234613486134553226482133236073603491413 737242870066363155841744815239239622292545917053643278925902605113301301488 204625213821429519415316024320522703627595939530921261373219226137931021185 580744023794627495273518257527348912274384830113491298346743644406563430865 2133494633522473749030217386093370277033921317659317670238267581846066440
  • Beatnik — стековый язык, код которого напоминает предложения на английском языке, часто не несущие никакой смысловой нагрузки. «Стоимость» слов определяется правилами игры Scrabble и используется для принятия решения о том, какую операцию необходимо выполнить:
Soars, larkspurs, rains. Indistinctness. Mario snarl (nurses, natures, rules...) sensuously retries goal. Agribusinesses' costs par lain ropes (mopes) autos' cores. Tuner ambitiousness. Flit. Dour entombment. Legals' saner kinking lapse. Nests glint. Dread, tied futures, dourer usual tumor grunts alter atonal garb tries shouldered coins. Taste a vast lustiness. Stile stuns gad subgroup gram lanes. Draftee insurer road: cuckold blunt, strut sunnier. Rely enure pantheism: arty gain groups (genies, pan) titters, tattles, nears. Bluffer tapes? Idle diatom stooge! Feted antes anklets ague? Remit goiter gout! Doubtless teared toed alohas will dull gangs' aerials' tails' sluices; Gusset ends! Gawkier halo! Enter abstruse rested loser beer guy louts. Curtain roams lasso weir lupus stunt. Truant bears animate talon. Entire torte originally timer. Redo stilt gobs. Utter centaurs; Urgent stars; Usurers (dilute); Noses; Bones; Brig sonar graders; Utensil silts; Lazies. Fret arson veterinary rows. Atlas grunted: "Pates, slues, sulfuric manor liaising tines, trailers, rep... unfair! Instant snots!" Sled rested until eatery fail. Ergs fortitude Indent spotter Euros enter egg. Curious tenures. Torus cutlasses. Sarong torso earns cruel lags it reeled. Engineer: "Erase handbag -- unite ratification!" oaring oaten donkeys unsold, surer rapid saltest tags BUTTERED TIBIA LUGS REWIRING TOILETS anion festers raring edit epilogues. DIRGE ROTOR. linnet oaring. GORE BOOTIES. Ironed goon lists tallest sublets -- Riots, Raucous onset. Ignobly, runners' diet anguishes sunrise loner. Erode mob, slier switcher! Loaners stilt drudge pearl atoll, risking hats' ends. Rebind sitters. Toga epistles -- crud lard. (Pager purse dons souls.) glob title a curio hired rites shed suds lade grease strut arctic revs toad unless idlers rind stilt region land GERMICIDES SULTANA GUTS gill siting leans nice spurs tests gloves roused asp Holes! Moles! (Sores!) Hygienists! Scars! (Asses!) Smells spell rares. Cubs instant sing in parse goodies. Rosin. Unhelpful sisal acres. Slope told. MALENESS PASTA LAB. "Infirmary vine," rang illiterates (beans). Rosin sours, insults truss abalones, nailed rules, helical atlases. Dear remodeling stings mar rents. Sunless shiner orb (silly idol.) Clarity disses senna. Vagabonds sauted; sloes performed gelds. Alter post radial lip sectioning gums. Saint Towellings. Larger aeons telephone stolid char, pal! Boats Dean forsook, rosters, tunas, terrariums -- united, traced. Nude pagoda careens.

Да-да, это сейчас был «Hello, World!»

  • Asdf использует всего четыре инструкции — на удивление, это a, s, d и f:
asaasaasssasaasssasaaaasssasaasssasaaaaaaaasssasaaaasssasaaaasssasaasssasaasss asaasssasaasssasaaaasssasaasssasaaaasssasaaaaaasssasaaaasssasaasssasaaaasssasa aaaaasssasaaaasssasaasssasaaaaaaaasssasaaaasssasaasssasaasssasaaaasssasaaaaaaa asssasaasssasaaaaaaaaaaaasssasaaaaaasssasaasssasaaaaaasssasaasssasaaaasssasaas ssasaaaaaaaasssasaasssasaaaaaasssasaaaasssasaasssasaaaasssasaaaasssasaasssasaa aasssasaaaaaasssasaaaasssasaaaasssasaasssasaaaaaaaasssasaasssasaaaaaaaasssasaa

Существует бессчетное множество таких языков, продолжить знакомство можно на вики Esolang. Настоятельно рекомендуем попытаться написать хотя бы парочку эзотерических программ — это неплохая тренировка для ума 🙂

На этом всё. Предлагайте в комментариях другие парадигмы, которые хотели бы увидеть в этой подборке, и мы постараемся ее дополнить.

Типичный программист.

Источник: Типичный программист