Бинарный салют, друзья! На связи EngineerSpock.
Любой новичок, который сталкивается с программированием начинает читать или отсматривать массу материалов в которых очень часто упоминаются такие понятия как императивное программирование, структурное программирование, процедурное, функциональное, объектно-ориентированное и так далее. Что это всё за такие понятия? Как языки связаны с этими понятиями, что из перечисленного лучше, а что хуже и что со всем этим делать мы и будем разбираться в этой статье. Поехали!
Лайкосы / Подписки / Курсы
- подписывайтесь на группу в телеграме
- подписывайтесь на канал в YouTube
- покупайте курсы по программированию
Начнём с того, что всё что я перечислил, называют парадигмами программирования. Парадигма – серьёзное словечко, которое по сути, если откинуть занудную заумь, в контексте программирования сводится к стилю программирования. Стиль же программирования в свою очередь – это о том каким образом мы компонуем между собой кусочки программы, какие конструкции в языке позволено использовать и какую семантику или другими словами смысловую нагрузку они несут. Это неточное, зато более понятное определение чем какая-нибудь сугубо научная дичь.
Этих самых стилей-парадигм великое множество, но мы выделим лишь основную часть. Давайте начнём с императивного программирования. Что это за программирование такое?
Императивное программирование
Императив – значит команда и по сути императивное программирование это когда есть последовательность команд, которая последовательно выполняется, причём результаты выполнения предыдущих команды могут быть прочитаны последующими. Кажется, будто бы все языки программирования в мире – императивные. И, возможно, вы удивитесь, но, по сути, так оно и есть. Любой язык программирования в той или иной степени является императивным. Ну, за всю историю языков было создано тысячи, поэтому не скажу за все, но бьюсь об заклад, что все известные вам языки – императивные. Java, C#, Python, Go, JavaScript, Delphi, перечислять можно бесконечно. А если язык программирования поддерживает параллельное исполнение команд это ещё не делает его не императивным, как вы можете догадаться. Ну, это логично.
Самое древнее программирование, предполагавшее последовательное написание голых ничем не прикрытых машинных инструкций – это вот то самое чисто императивное программирование без прикрас.
Процедурное программирование
Писать машинные инструкции это совсем невесело, чревато багами и много чем ещё, поэтому программисты решили, что неплохо было бы как-то объединять одни и те же куски кода, чтобы не дублировать их по сто раз и так возникло процедурное программирование – парадигма, позволяющая писать процедуры, или функции если угодно. В общем и целом считается, что процедура может иметь входные аргументы, но не может иметь выходных, а функция в отличие от процедуры может иметь и те и другие. И тут вы скажите – так, минуточку, разве не все известные языки программирования – процедурные? Они же все позволяют писать процедуры. И, как ни странно, вы будете почти что правы. Почти все современные и не только современные языки программирования – процедурные, поскольку позволяют создавать, собственно, процедуры. Процедуры могут быть запрещены в чисто функциональных языках типа Haskell. Хотя, я не гуру хаскела, но допускаю, что его строго говоря, нельзя отнести к процедурным языкам. Но я могу ошибаться. В независимости от этого, с уверенностью могу сказать, что большинство даже функциональных языков программирования, о которых мы ещё поговорим через пару минут, позволяют с помощью хаков встроенных в сами эти языки писать процедурный код. Даже ассемблер по крайней мере частично поддерживает процедурное программирование за счёт возможности писать макросы, которые даже аргументы принимать умеют.
Функциональное и декларативное программирование
Наряду с процедурным программированием, примерно в то же время, в конце 50х годов родилась парадигма функционального программирования, которую относят к подкатегории декларативного программирования. Причём мне трудно сказать когда именно было формализовано понятие декларативного программирования, до появления функционального или после. Но не суть важно. Декларативное программирование подразумевает, что программист описывает что должна делать программа, чтобы решить задачу. Императивное же программирование, в отличие от декларативного подразумевает, что программист рассуждает в терминах «как» программа решает задачу, не что, а как. Одним из ярких представителей декларативного программирования является язык SQL. Вернёмся к функциональной парадигме. Функциональное программирование поскольку является подкатегорией декларативного в общем и целом так же исповедует подход, подразумевающий, что программа говорит не «как», а «что» делать, чтобы решить задачу. Такой подход позволяет конструировать программу, связывая друг с другом функции, выход одной функции является входом для другой и так далее. Комбинация функций является программой, которая и решает задачу. Чтобы подобное стало возможным, функциональный язык программирования должен навязывать определённые ограничения: например, функции должны быть чистыми, они не должны изменять никаких состояний, они должны принимать аргументы, и возвращать результат, они не должны лезть куда-то во вне и что-то там менять. То есть код функции не выходит за её собственные пределы. В терминах этой парадигмы, у нас даже может не быть переменных, то, что в других языках подразумевают под переменными, здесь называют значениями. И когда мы, казалось бы, с точки зрения синтаксиса присваиваем результат выполнения функции переменной, мы на самом деле связываем значение с функцией. То есть, присвоение может вообще отсутствовать как понятие. По сути, в программе, построенной на основе этой парадигмы, вообще не должно быть какого-то разделяемого состояния, которое могут изменять функции. В теории, с таким подходом к разработке, можно написать программу и доказать её полную корректность. На практике же, подавляющее большинство функциональных языков программирования в той или иной степени разрешают отходить от таких жёстких ограничений, позволяя объявлять изменяемые переменные, например, и так далее. Что касается верификации корректности программы вообще и верификации программы, написанной на функциональном языке в частности – тема отдельной статьи.
Короче говоря, функциональная парадигма предполагает, что все функции в программе – строго математические, у них есть входные значения, есть выходные и эти функции не лезут за свои пределы, они не мутируют какое-либо состояние.
Из функциональных языков можно выделить старый могучий LISP с жутким синтаксисом, состоящим из несметного количество скобочек, Haskell, Erlang, Clojure, F# и так далее. Какие-то функциональные языки считаются чистыми, какие-то позволяют отходить от чистой функциональщины и заниматься тем, что как-бы под запретом. Про функциональное программирование у нас планируется отдельная серия статей.
Структурное программирование
Процедурное и функциональное программирование облегчило жизнь древних программистов, но сложность программ росла и одних процедур стало недостаточно. Программисты требовали жертв, шутка, программисты требовали больше абстракций и пришли к структурному программированию. Структурное программирование в отличие от процедурного это уже принципиально иной уровень в области разработки программ. Это важнейшая веха в истории программирования.
Увидев слово «структура» – можно подумать, что речь идёт здесь о структурах, представляющих собой некие сущности. Ну, то есть, как классы, только структуры со своими ограничениями, типа отсутствия наследования. Но нет, под словом структура понимается скорее особое структурирование программ с использованием следующих ключевых механизмов:
- последовательность,
- ветвление,
- цикл,
- подпрограммы в виде процедур и функций,
- блоки кода.
Итальянские математики Бём и Якопини доказали теорему, названную в их честь о том, что алгоритм любой сложности может быть запрограммирован с использованием простых последовательностей команд, ветвлений с помощью if else и цикла while. Можно заметить, что в данном утверждении не упоминается оператор goto, очень активно использовавшийся в эпоху «доструктурного» программирования. Оператор goto позволяет произвольно скакать от одного куска программы к другому, из-за чего понять последовательность исполнения в больших программах становилось непосильной задачей. Широкое использование goto приводило к так называемому спагетти-коду. Важным постулатом структурного программирования является постулат о вредности оператора goto и о том, что ни в каких программах его использовать не надо. Goto это чистое зло. Под блоками кода в структурном программировании понимаются, собственно блоки кода, разграниченные, например, ключевыми словами begin end или просто фигурными скобками как в языке С. Блоки позволяют логически отделять кусочки кода друг от друга, а также ограничивать области видимости переменных и функций. Одним из самых ярких родоначальников этой парадигмы был Эдгар Дейкстра, написавший немало очень важных работ на эту тему. Самым ярким представителем структурного программирования среди языков стал старый добрый C. С не просто обладал всеми ранее упомянутыми возможностями, но и позволял строить переиспользуемые сущности, оформляемые в виде так называемых структур, где можно было группировать данные и функции, относящиеся друг к другу. В общем и целом же, снова можно сказать, что огромное количество современных языков программирования включают в себя структурную парадигму, то есть позволяют писать программы в данном стиле, полностью или частично. Одним из современных представителей данной парадигмы является язык Go. Однако не стоит забывать, что C# тоже умеет структурное программирование. Другое дело, что C# умеет гораздо больше. Но обо всём по порядку.
ООП
Можно было бы предположить, что ещё более сложная парадигма объектно-ориентированного программирования выросла из парадигмы структурного программирования, но это было бы ошибкой. ООП было концептуализировано примерно в конце 60х годов, так же как и структурное программирование. То есть эти парадигмы появились примерно в одно и то же время. ООП так же как и структурное программирование стало ответом на растущую сложностью программ. ООП в отличие от структурного программирования вводит понятия классов и объектов, каждый из которых является экземпляром класса, а также важнейшие столпы ООП:
- инкапсуляция,
- наследование
- полиморфизм.
С помощью наследования мы можем строить сложные иерархии, позволяющие смоделировать отношения родителя, дающего базовый функционал и наследника, расширяющего этот функционал. Полиморфизм даёт гибкость за счёт возможности взаимозаменяемо использовать совместимые друг с другом классы. Совместимость эта в разных языках может реализовываться по разному и зависит в том числе от типизации о которой мы поговорим в отдельному ролике.
Короче говоря, ООП даёт огромную гибкость в том как мы структурируем программы.
Первым языком воплотившим концепции ООП стал язык Симула-67, хотя он и не является каноническим ООП языком. Но зато точно можно сказать, что первым полноценным и действительно популярным языком, поддерживающим ООП стал язык Smalltalk.
Объектно-ориентированная парадигма довлеет в сфере разработки ПО уже несколько десятилетий. Языки поддерживающие ООП, такие как Java, C#, Python позволяют решать проблемы, по сути, любых масштабов. Именно это стало причиной такой популярности объектно-ориентированного программирования.
Ренессанс функционального программирования
Отдельно хотелось бы сказать, что где-то примерно с 2014 года потихоньку начался ренессанс функциональной парадигмы. В C# в своё время пришли лямбда-выражения, был сделан LINQ framework, который был и остаётся воплощением идей функционального программирования. Он позволяет делать запросы к объектам, базам данных, строя цепочки вызовов. В последующие годы вплоть до сегодняшнего момента практически в каждом новом релизе C# приобретал всё новые фичи, свойственные функциональной парадигме. Множество идей было почерпнуто из F#, функционального языка платформы .NET. На фоне происходящего, в Java тоже начали затаскивать функциональщину. Даже в С++ притащили лямбды, в Python есть лямбды. Хотя, чего греха таить, например C#, конечно, гораздо больше впитал функциональщины, чем тот же Python. Но тем не менее, это не меняет того, что тренд на изучение и имплементацию концепций функционального программирования есть. Функциональный стиль позволяет писать декларативный, понятный и надёжный код.
Заключение
Из всего вышесказанного, становится ясно, что большинство топовых популярных сегодня языков общего назначения являются мультипарадигмальными в полном смысле этого слова.
C#, например, не просто притащил к себе одну-две фичи из мира функционального программирования, он притащил целую гору таких фич.
И таких примеров можно приводить ещё много. Одни парадигмы не хуже других, они вполне комфортно могут сосуществовать и дополнять друг друга. ООП на функциональных стероидах, например, позволяет писать очень надёжный и лёгкий в сопровождении код.
Есть и другие важные парадигмы – такие как метапрограммирование, аспектно-ориентированное программирование и так далее. Но об этом, видимо, будем говорить отдельно, поскольку нельзя впихнуть невпихуемое в рамки одной статьи.
Несмотря на то, что мы разобрали не всё, что хотелось бы – мы всё же обсудили основные парадигмы, то как они появлялись, по какой причине, и какие преимущества они дают. Надеюсь, теперь вы перестанете озадачиваться, встречая в статьях или книгах, страшные названия различных парадигм программирования.