Как изучить в программировании всё? Для того нужен системный подход, основывающийся в идеале на некоторой научной базе. Один из хороших академических методов -- понимание программирования как целостной системы programming in small и programming in large через парадигмы и концепции программирования, напрямую применимые и к разработке, и проектированию программных систем любой сложности.
Эта серия статей -- быстрое введение в основные парадигмы программирования, лежащие в их основе концепции и, главное, отношения между ними. Изучив их, вы сможете достаточно осознанно выбирать парадигму (и, по сути, архитектуру) или их комбинацию, наиболее подходящую под конкретный проект или задачу. Важно, что многие из этих парадигм уже воплощены в каком-то формате в широко распространённых языках (Java, Python, C++, C# ...) и популярных фреймворках, и их можно выбирать сознательно, не уходя в экзотические технологии. Более подробно тема парадигм программирования и интеграции programming in small и programming in large изучается на отдельных курсах в моей Школе Программирования.
Далее будет приведена своеобразная таксономия нескольких десятков полезных парадигм программирования и их взаимосвязей друг с другом. Вы узнаете, как парадигмы влияют на дизайн языка, и какие языковые возможности лучше применять под различные задачи.
Мы познакомимся с главными программистскими концепциями: записи (records), замыкания (closures), именованное состояние (named state), независимость/одновременность/параллелизм * (concurrency) и др.
- Далее в качестве concurrency я буду использовать термин "параллелизм" (параллельное выполнение), хотя более корректным, но менее наглядным будет "одновременность". Где подразумевается именно оригинальный параллелизм, будет отмечено особо.
Мы познакомимся с основными принципами построения абстракций данных, и выясним, как они помогут проектировать большие программы.
В заключение мы сосредоточимся на параллельном выполнении, которое считается самой трудной концепцией в программировании. В частности, мы немного коснёмся четырёх малоизвестных, но важных парадигм, которые значительно упрощают параллельное программирование при использовании популярных языков: декларативный параллелизм (включая энергичный и ленивый), функциональное реактивное программирование, дискретное синхронное программирование, и программирование в ограничениях. Эти парадигмы особенно круты потому, что могут быть использованы, когда ни одна другая парадигма не срабатывает. В частности, они особо актуальны для правильной разработки параллельных программ для многоядерных процессоров.
По материалам вечной классики: учебника
"Concepts, Techniques, and Models of Computer Programming" (СТМ)
Что такое парадигма программирования
Причина, по которой программистских парадигм много, в том, что для разных классов задач нужны качественно разные способы их решения, иначе эффективность, производительность, простота решения будет сильно ухудшаться.
Парадигма программирования -- это подход к программированию (способ программирования, разработки программной системы), который основан либо на математической теории, либо на логически хорошо формализованном когерентном (согласованном, взаимосвязанном) множестве принципов. Каждая парадигма поддерживает набор оригинальных концепций, которые делают её лучшей для определенного класса задач и проектов. Почти всегда парадигма -- это не что-то инженерное, придуманное исходя из опыта, а математическая теория, которая была открыта учёными. По сути, каждая парадигма -- это научное открытие.
Например, объектно-ориентированное программирование лучше всего подходит для создания систем, где подразумевается большое число взаимосвязанных абстракций данных, организованных в иерархии. Логическое программирование лучше всего подходит для анализа и преобразования сложных символических структур в соответствии с наборами логических правил. Дискретное синхронное программирование лучше всего подходит для "реактивных" задач, когда в системе постоянно происходят реакции на последовательности внешних событий, которые (реакции) после некоторых вычислений генерируют выходные события. Отличие от популярного сегодня функционального реактивного программирования FRP в том, что тут "время" в системе прерывно (шаг времени -- это, как правило, произвольный период между двумя входными событиями), в отличие от FRP, где время непрерывно. По этой причине FRP также называют непрерывным синхронным программированием.
Некоторые известные языки, которые поддерживают эти три парадигмы, соответственно, Java, C# , C++ (ООП), Prolog, Planner, Datalog (логическое программирование), Esterel, Lustre, Argos, Signal (дискретное синхронное программирование). Кроме того, существует немало библиотек, дополняющих языки соответствующими возможностями.
Несколько парадигм в одном языке
В широко распространенных мэйнстримовских языках вроде Java или C++ исходно полноценно поддерживалась одна-две парадигмы, и это было плохо. Для решения задач разных классов гораздо лучше и продуктивнее использовать разные парадигмы, более точно подходящие под соответствующие классы, позволяющие решать соответствующие задачи ясно и чисто.
Из этого следует контринтуитивная идея, что идеальный язык должен поддерживать много разных концепций хорошо продуманным способом, чтобы программист мог выбирать из них наиболее подходящие, когда именно они будут нужны, и не обременяясь при этом помехами остальных концепций. Такой стиль иногда называют мультипарадигмальным программированием, и раньше обычно подразумевалось, что этот подход экзотический и необычный.
Однако буквально за последние 10-15 лет ведущие языки серьёзно расширились возможностями самых разных парадигм. Например, в Java или Си шарп уже по 6-8 парадигм, а в С++ с учётом возможностей, предоставляемых стандартными библиотеками, оно возрастает до 15.
Мультипарадигмальные лидеры сегодня -- это Oz (11 парадигм), Raku/Perl (10), Julia (9/17), Haskell (8/15), Kotlin (8), C++ (7/15), F# (7/8). Это не очень заметный тренд, так как программисты народ весьма консервативный, и цепляются за одну-две парадигмы (в 95% императивщина + ООП). При том, что в подавляющем большинстве проектов решается куда больше одного типа задач под одну парадигму. В любом проекте хотя бы даже на тысячу строк найдутся подсистемы, выражаемые лучше всего в различных парадигмах.
Представление языка непосредственно через концепции, без парадигмы, будет некорректным: именно парадигма, имеющая в своей основе, как правило, формальную теорию, и определяет качественно отличный от других стиль программирования. Нередко две парадигмы, которые считаются или выглядят совершенно разными (например, функциональное и объектно-ориентированное программирование), отличаются только в одной концепции.
В общем случае, главная идея в том, что для построения достаточно сложной системы надо иметь хорошее множество парадигм, подходящих для простой, ясной и продуктивной реализации её различных подсистем. И даже если ваш язык программирования некоторую парадигму не поддерживает, её часто можно неплохо сэмулировать (условно говоря, и на Си можно программировать в объектно-ориентированном стиле). Ну и, к счастью, современные языки становятся всё более мультипарадигмальными.
Весьма подробно парадигмы и концепции рассматриваются в книге CTM, а данный цикл заметок служит введением в неё, дополняя некоторыми новыми понятиями. В оригинальной CTM все темы рассматриваются с использованием языка Oz, который много лет был хорошим мультипарадигмальным лидером, однако сегодня на его место серьёзно претендует Julia (а с учётом расширений и библиотек она уже существенно обогнала Oz).
Имея N концепций, теоретически можно сконструировать 2^N парадигм. Среди них будут явно бесполезные (с нулём или одной концепцией), и с практических позиций, нужны будут Тьюринг-полные парадигмы. Поэтому, в частности, функциональное программирование так важно: оно базируется на концепции функций первого класса, или замыканий, которые делают его эквивалентным лямбда-исчислению, которое Тьюринг-полное. В итоге, количество практически применимых парадигм будет значительно меньше, чем 2^N, хотя и существенно больше, нежели N.
Грамотное сочетание нескольких парадигм в одном языке/проекте -- задача весьма специфическая, и вот Julia в частности через метапрограммирование это отлично поддерживает. Умение проектировать языки с несколькими парадигмами -- это по сути умение проектировать большие и сложные системы, разные подсистемы которых практически всегда реализуются в разных парадигмах. Например, классическая система, состоящая из базы данных, бизнес-логики, сетевого highload-движка и клиентской части, может быть представлена в виде таких слоёв, как, например, реляционно-логическое программирование, последовательное и параллельное ООП, функциональное реактивное программирование. Уже исходя из такой архитектуры, можно сознательно подбирать наиболее подходящие фреймворки, отбирая их по формальным критериям совместимости фундаментальных концепций, и получая существенный выигрыш в сравнении с несознательным механическим выбором популярного стека.
продолжение следует