ВЛИЯНИЕ ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ НА СОВРЕМЕННЫЕ ЯЗЫКИ ПРОГРАММИРОВАНИЯ Текст научной статьи по специальности «Компьютерные и информационные науки»
ФУНКЦИОНАЛЬНОЕ ПРОГРАММИРОВАНИЕ / С++ / PYTHON / HASKELL / ПАРАЛЛЕЛЬНЫЕ ВЫЧИСЛЕНИЯ / ЯЗЫКИ ПРОГРАММИРОВАНИЯ / ПАРАДИГМЫ ПРОГРАММИРОВАНИЯ / ЛЯМБДА-ФУНКЦИЯ / МОНАДИЧЕСКИЙ ИНТЕРФЕЙС / АСИНХРОННОСТЬ / ЛЕНИВАЯ ОЦЕНКА
Аннотация научной статьи по компьютерным и информационным наукам, автор научной работы — Телегин В.А.
Статья посвящена исследованию влияния функционального программирования на современные языки программирования. Автором обосновывается актуальность и значимость темы исследования. Современные языки программирования обладают большим набором различных инструментов и полезных функций, которые позволяют писать совершенно другой код на том же языке, для одной и той же задачи. Парадигма программирования — это, в первую очередь, способ мышления — то, как программист думает о представлении и обработке. Другими словами, парадигма программирования существует в сознании программиста; она не является частью языка. Разные языки могут в разной степени поддерживать определенную парадигму. В статье постулируется о том, что в настоящее время, функциональное программирование было актуальным направлением в разработке программного обеспечения с самых ранних дней, но приобрело новое значение в современную эпоху. Рассматриваются концепции, лежащие в основе функционального программирования . Заключается о том, что в современном мире, функциональное программирование переживает «ренессанс» из-за того, что функциональные языки позволяют писать декларативный, понятный и надежный программный код. Особый акцент делается на положении о том, что тренд на изучение и имплементацию концепций функциональной парадигмы существует в современных языках программирования общего пользования, что делает большинство современных языков программирования — мультипарадигмальными. В целом, анализ научной литературы позволил сделать вывод о том, что языки программирования движутся вперед и активно развиваются, накапливая новые, все более совершенные и удобные инструменты. Очевидно, что популярные языки, такие как Python и С ++, приобретают все большее количество функций, которые произошли от функционального программирования .
i Надоели баннеры? Вы всегда можете отключить рекламу.
Похожие темы научных работ по компьютерным и информационным наукам , автор научной работы — Телегин В.А.
О представлении модельного времени при помощи механизмов функционального программирования
О ФУНКЦИОНАЛЬНОМ ПРОГРАММИРОВАНИИ
ОБОБЩЕННОЕ ПРОГРАММИРОВАНИЕ С КОМБИНАТОРАМИ И ОБЪЕКТАМИ
Скриптовый язык «Link» для программирования сценариев исполняемых событийных систем
ФУНКЦИОНАЛЬНО-ИМПЕРАТИВНЫЙ ЯЗЫК ПРОГРАММИРОВАНИЯ EL
i Не можете найти то, что вам нужно? Попробуйте сервис подбора литературы.
i Надоели баннеры? Вы всегда можете отключить рекламу.
THE INFLUENCE OF FUNCTIONAL PROGRAMMING ON MODERN PROGRAMMING LANGUAGES
The article is devoted to the study of the influence of functional programming on modern programming languages. The author substantiates the relevance and significance of the research topic. Modern programming languages have a large set of different tools and useful features that allow you to write completely different code in the same language for the same task. A programming paradigm is, first of all, a way of thinking — how the programmer thinks about representation and processing. In other words, the programming paradigm exists in the mind of the programmer; it is not part of the language. Different languages may support a particular paradigm to varying degrees. The article postulates that at present, functional programming has been a hot trend in software development since its earliest days, but has taken on a new meaning in the modern era. The concepts underlying functional programming are considered. It concludes that in the modern world, functional programming is experiencing a «renaissance» due to the fact that functional languages allow you to write declarative, understandable and reliable program code. Particular emphasis is placed on the position that the trend towards the study and implementation of the concepts of the functional paradigm exists in modern general-use programming languages, which makes most modern programming languages multi-paradigm. In general, the analysis of scientific literature led to the conclusion that programming languages are moving forward and actively developing, accumulating new, more and more advanced and convenient tools. Obviously, popular languages like Python and C++ are acquiring more and more features that have evolved from functional programming.
Текст научной работы на тему «ВЛИЯНИЕ ФУНКЦИОНАЛЬНОГО ПРОГРАММИРОВАНИЯ НА СОВРЕМЕННЫЕ ЯЗЫКИ ПРОГРАММИРОВАНИЯ»
Влияние функционального программирования на современные языки программирования
Телегин Валентин Александрович
технический директор департамента мобильной разработки, ООО «Ростелеком Информационные Технологии», valentin.telegin.it@gmail.com
Статья посвящена исследованию влияния функционального программирования на современные языки программирования. Автором обосновывается актуальность и значимость темы исследования. Современные языки программирования обладают большим набором различных инструментов и полезных функций, которые позволяют писать совершенно другой код на том же языке, для одной и той же задачи. Парадигма программирования — это, в первую очередь, способ мышления — то, как программист думает о представлении и обработке. Другими словами, парадигма программирования существует в сознании программиста; она не является частью языка. Разные языки могут в разной степени поддерживать определенную парадигму. В статье постулируется о том, что в настоящее время, функциональное программирование было актуальным направлением в разработке программного обеспечения с самых ранних дней, но приобрело новое значение в современную эпоху. Рассматриваются концепции, лежащие в основе функционального программирования. Заключается о том, что в современном мире, функциональное программирование переживает «ренессанс» из-за того, что функциональные языки позволяют писать декларативный, понятный и надежный программный код. Особый акцент делается на положении о том, что тренд на изучение и имплемента-цию концепций функциональной парадигмы существует в современных языках программирования общего пользования, что делает большинство современных языков программирования — мультипарадиг-мальными. В целом, анализ научной литературы позволил сделать вывод о том, что языки программирования движутся вперед и активно развиваются, накапливая новые, все более совершенные и удобные инструменты. Очевидно, что популярные языки, такие как Python и С ++, приобретают все большее количество функций, которые произошли от функционального программирования. Ключевые слова: функциональное программирование, С++, Python, Haskell, параллельные вычисления, языки программирования, парадигмы программирования, лямбда-функция, монадический интерфейс, асинхронность, ленивая оценка.
Как известно, функциональное программирование (далее — ФП) — одна из первых парадигм, нацеленных не просто на получение эффективной реализации заранее подготовленных алгоритмов, а на решение новых задач информационной обработки, обладающих исследовательским компонентом.
Функции являются фундаментальными для организации кода; они существуют во всех языках программирования более высокого порядка. Как правило, функциональное программирование означает использование функций с наилучшим эффектом для создания чистого и поддерживаемого программного обеспечения. Более конкретно, функциональное программирование — это набор подходов к кодированию, обычно описываемых как парадигма программирования.
Функциональное программирование иногда определяется в противовес объектно-ориентированному программированию (ООП) и процедурному программированию. Это вводит в заблуждение, поскольку эти подходы не являются взаимоисключающими, и большинство систем, как правило, используют все три.
Функциональное программирование предлагает очевидные преимущества в определенных случаях, оно широко используется во многих языках и фреймворках и занимает видное место в современных тенденциях программного обеспечения. Это полезный и мощный инструмент, который должен быть частью концептуального и синтаксического инструментария каждого разработчика.
1. Функции современных языков ФП
1.1. Функции первого класса
Следует согласиться с мнением А.Э. Романовой о том, что язык программирования — это формальный язык, который необходим для написания разного рода компьютерных приложений, служб и драйверов [1]. Все языки программирования состоят из определенных компонентов и функций.
Отличительной чертой функционального программирования, в целом, является широкое использование функций, которые превратились в основной инструмент разработки. С нашей точки зрения, следует для начала рассмотреть основные определения, описывающие различия между функциями и процедурами, а также другие подобные конструкции нефункциональных языков.
Так, к примеру, функции высшего порядка — это действия, которые либо принимает другую функцию в качестве аргумента, либо возвращает некоторую функцию в качестве результата. Их также называют функционалами. Такое поведение может быть реализовано даже в чистом C с помощью указателей на функции (см. рис. 1).
void update_user_balance(int user_id,
Какой вклад внесло функциональное программирование в современные языки?
Современные языки программирования обладают большим набором разнообразных средств и удобных фишек, что позволяет писать совершенно разный код на одном и том же языке для одной и той же задачи.
Парадигма программирования — это в первую очередь стиль мышления: то, как программист думает о представлении данных и процессе их обработки. Другими словами, парадигма живёт в голове программиста, а не является свойством языка. Разные языки могут в той или иной степени поддерживать определённую парадигму. Если сейчас зайти на Википедию и начать читать про самые популярные ЯП, мы увидим, что многие из них заявлены как «мультипарадигменные»: на них можно писать в разных стилях, но какие-то из них использовать будет удобнее.

В своей недавней статье мы рассказывали о практических применениях Лиспа и упомянули, что он сильно повлиял на развитие других языков программирования, но не стали вдаваться в детали. Пришло время более подробно раскрыть эту тему и разобраться, какой вклад функциональное программирование в целом (не только Лисп!) внесло в развитие других языков. Поскольку мы используем Haskell как основной язык разработки, и наша команда разработчиков состоит из ФП-энтузиастов, мы не смогли пройти мимо такой темы.
В этом посте рассмотрим несколько механизмов, которые либо зародились в ФП-языках, либо нашли в них наибольшее применение и были ими популяризованы, и в итоге появились в языках, изначально не функциональных.
Функции первого класса
Отличительная особенность ФП-стиля в целом — это широкое применение функций, которые становятся одним из самый главных инструментов разработки. Давайте быстро пробежимся по основным определениям, которые описывают различия функций от процедур и других похожих конструкций из не-функциональных языков.

Функция высшего порядка (higher-order function) — это такая функция которая либо принимает другую функцию в виде аргумента либо возвращает функцию в результате. Их ещё называют функционалами. Такое поведение можно реализовать даже в чистом С, используя указатели на функции:
void update_user_balance(int user_id, double (*update_fn)(double)) < // . user->balance = update_fn(user->balance); // . >
Функция первого класса (first-class function) — те, которыми можно манипулировать как со всеми другими значениями: передавать как аргументы, возвращать в качестве результата, присваивать переменным и полям структур.
Безымянная функция (lambda function) — это функция без названия . Кроме отсутствия имени поддержка безымянных функций снимает другие ограничения языка на объявление функций (в некоторых языках, например, в стандарте C99, объявления функции могут встречаться только на верхнем уровне). Поддержка безымянных функций предполагает, что функция может быть объявлена в любом месте где валидны обычные выражения. Безымянные функции чаще всего используются в функционалах, их совместное использование очень удобно и позволяет сильно сократить код.
// Пример использования безымянной функции для печати содержимого // std::vector int main() < std::vectorv; v.push_back(1); v.push_back(2); std::for_each(v.begin(), v.end(), [] (int x) < std::cout ); >
Замыканиe (closure) — функция может захватить некоторые переменные из контекста в котором она была объявлена, не позволяя сборщику мусора уничтожить данные которые могут быть использованы в этой функции до тех пор пока в приложении существует ссылка на саму функцию. Пример на TypeScript:
function createClosures() < // Переменная видна внутри функций ниже let counter = 0; // Значения полей inc и print являются замыканиями return < inc: () =>< counter++; >, print: () => console.log('counter value: ' + counter), >; > const c = createClosures(); c.inc(); c.inc(); c.print(); // >>> "counter value: 2"
Абстракция списков
Абстракция списков (list comprehension) позволяет компактно записывать обработку или генерацию списков из уже существующих. Одним из первых языков, в котором использовался такой синтаксис, была Miranda, из которой его позаимствовал Haskell, а затем подобные конструкции стали появляться в «менее функциональных» языках, таких как Python, C#, Ruby.

В качестве примера рассмотрим код на Python и Haskell, который составляет словосочетания из прилагательных и существительных. Эти два фрагмента очень похожи и отличаются только синтаксическими мелочами, не правда ли?
# Пример на python nouns = ["waterfall", "moon", "silence"] adjectives = ["late", "divine", "blue"] phrases = [a + " " + n for n in nouns for a in adjectives] # >>> ['late waterfall', 'divine waterfall', 'blue waterfall', 'late moon', 'divine moon', 'blue moon', 'late silence', 'divine silence', 'blue silence']
-- То же самое на haskell nouns = ["waterfall", "moon", "silence"] adjectives = ["late", "divine", "blue"] phrases = [a ++ " " ++ n | n >> ['late waterfall', 'divine waterfall', 'blue waterfall', 'late moon', 'divine moon', 'blue moon', 'late silence', 'divine silence', 'blue silence']
Алгебраические типы данных
Также эти типы могут называться ADT-типами, типами-суммами, дискриминированными объединениями, дизъюнктивными объединениями, копроизведениями, а может ещё какими-то умными словами. Вы можете быть знакомы с идеей таких типов под разными названиями, но, если говорить коротко, то это составной тип, который содержит поле-дискриминант (можно назвать тегом) вместе с ассоциированными с ним данными. Ниже пример на Haskell такого типа-объединения, описывающего возможные действия пользователя в гипотетической реализации приложения TodoMVC. Часть действий несут в себе «полезную нагрузку» (строку или ID элемента).
data UserAction = TextInput String | EnterPressed | EscPressed | CheckTodoItem ItemId Bool | EditTodoItem ItemId String | DeleteTodoItem ItemId | ApplyFilter TodoFilter
Несмотря на простоту и полезность в моделировании сущностей предметной области, поддержку ADT редко можно встретить в полном объеме в популярных языках и в базах данных. Вот некоторые примеры которые реализуют похожие типы: Enum в Rust, Sealed Classes в Kotlin, std::variant в C++
Сопоставление с образцом
Cопоставление с образцом (Pattern Matching) — это синтаксическая конструкция, которая позволяет получить доступ к данным структуры, состоящей из одного или нескольких вариантов с разным набором полей (те самые ADT, алгебраическая сумма типов, enum, std::variant и т.д., про которые говорится в предыдущем пункте). Pattern Matching напоминает всем знакомый из императивных языков оператор switch-case, но его главное преимущество в том, что доступ к полям вариантов проверяется компилятором статически, используя информацию о типе выражения, в то время как switch-case не позволяет избежать ошибок с некорректным доступом к полям, отсутствующими case’ами или избыточными проверками.
Pattern Matching — ещё один прием который был популяризован в функциональных языках, где показал свою практичность и в настоящее время в разных формах активно заимствуется и интегрируется в Python, Java, C#, Rust и в других популярных языках.
-- Пример функции обновления состояния в гипотетическом TodoMVC -- написанном в стиле архитектуры Elm. Сопоставление с Образцом -- используется для анализа события сгенерированного пользователем. -- Событие имеет тип UserAction, который мы описали выше как пример АТД. updateTodoList :: UserAction -> TodoState -> TodoState updateTodoList action oldState = case action of TextInput newTitle -> oldState EnterPressed -> appendNewItem oldState EscPressed -> stopEditing oldState CheckTodoItem itemId itemChecked -> updateItemById itemId (#checked .~ itemChecked) EditTodoItem itemId itemTitle -> updateItemById itemId (#title .~ itemTitle) DeleteTodoItem itemId -> deleteTodoItembyId itemId oldState ApplyFilter newFilter -> oldState
Ленивые вычисления
В большинстве ЯП вычисление значения происходит в момент его присвоения переменной, все аргументы вычисляются перед вызовом функции (строгие вычисления). Альтернативный подход — «ленивый», когда вычисление значения откладывается до его использования. Ленивые вычисления позволяют работать с бесконечными структурами данных, писать декларативный код с определениями, организованными в порядке, удобном для чтения, а не в порядке их вычисления. Если вы использыете DSL подход, ленивые вычисления помогают легко реализовать такие конструкции как if-then-else (будет вычисляться значение только в нужной ветке).
История термина уходит корнями в лямбда-исчисление, одну из теоретических основ ФП, поэтому неудивительно, что в основном оно используется в ФП-языках. Например, в Haskell всё вычисляется лениво по-умолчанию.
Элементы «ленивости» можно найти и в других языках, даже в чистом Си логические операторы && и || ленивые: не вычисляют свой второй аргумент, если первый вычислился в 0 или 1 соответственно. В более высокоуровневых языках чаще используется термин «отложенные вычисления», которые реализованы с помощью функций-генераторов и ключевого слова yield. Такие генераторы есть, например, в Python или в Java
Континуации
Континуация (продолжение) представляет собой «остаток вычислений», т.е. для каждого подвыражения в программе описывается то, что осталось сделать с результатом этого выражения. Выражение получает континуацию в виде дополнительного аргумента, и, когда получен результат, текущая функция вызывает переданную континуацию с вычисленым значением вместо прямого возврата результата. Такой стиль передачи результата называется Continuation-passing style (CPS).
// Прямой стиль передачи результата function getFoo(): Foo // CPS-стиль function getFooCPS(cont: (foo: Foo) => A): А
CPS стиль редко встречается непосредственно в исходном коде программ. Одна из главных областей его использования — в компиляторах, как промежуточный формат перед генерацией машиного кода. Перевод кода в CPS позволяет преобразовать рекурсивные вызовы функций к хвостовой рекурсии, которую легко оптимизировать так, чтобы при вычислениях не рос стек.
Континуации сами по себе являются очень мощным инструментом, с помощью которого можно реализовать управляющие конструкции, такие как преждевременный выход из функции, явный вызов хвостовой рекурсии, императивные циклы и другие. Более подробно про использование континуаций можно посмотреть тут на примере языка Scheme.
Futures and promises
Futures, Promises, Deferred, а далее просто промисы являются конструкцией, которая содержит вычисление асинхронного значения. Они возникли в фунциональном программировании как инструмент для упрощения паралельных вычислений и выполнения запросов в распределенных системах.
const getJson = url => fetch(url).then(resp => resp.json()); // Отправка 2-х последовательных запросов const getUserInfo = getJson('/current-user-id').then( userId => getJson(`/user-info/$`).then( userInfo => console.log(`Hi $, your id is $`) ) );
Промисы были популяризованы во многом благодаря их адаптации и широкому применению в браузере. Исполнение JavaScript в браузере ограничено только одним потоком выполнения и ожидание ответа HTTP-запросов в блокирующем стиле, как принято в большинстве платформ, приводило бы к зависанию страницы и раздражению пользователей. По этой причине для обработки ответов HTTP-запросов в браузере используются коллбек-функции. В то же время комбинировать такие запросы не очень удобно, и для описания кода, ставшего нечитаемым из-за большого количества коллбеков, возник термин «Ад обратных вызовов» (Callback hell). Промисы позволили частично решить проблему с отправкой параллельных запросов и последовательной цепочкой запросов:
// Отправка 3-х параллельных запросов const fetchInParralel = Promise.all([ getJson('/current-user-info'), getJson('/shopping-cart'), getJson('/recently-viewed-items'), ]).then(([userInfo, cart, viewedItems]) => < // Отобразить страницу используя полученную с сервера информацию // . >)
Во многих популярных языках (например C#, Java, JavaScript) промисы стали основным инструментом для асинхронного программирования.
Монадический интерфейс
Названия многих конструкций и приемов программирования в Haskell были заимствованы из теории категорий и других областей математики. Один из таких терминов — «Монада» стал предметом многих мемов и шуток про функциональное программирование. В сети существуют множество статей с объяснением, что такое «Монада» в функциональных языках и как их использовать.

Если же попытаться дать определение в общепонятных терминах, «Монада» — это просто интерфейс с двумя методами, которые позволяют связывать вычисления в цепочку как это делается на примере цепочки промисов. Промисы сами тоже являются примером реализации монадическиго интерфейса. В разных языках монадические вычисления могут иметь разные названия, например, bind , chain или pipe .
// Пример монады для генерации псевдослучайных значений, параметр А — // тип генерируемого значения class Random < // Создание Random из произвольного значения static of(value: A): Random // Метод для реализации цепочки вызовов chain(this: Random, then: (a: A) => Random): Random > declare function randomNumber(min: number, max: number): Random; declare const randomString: Random; // Пример использования монадной цепочки const randomUser: Random = randomString.chain( userName => randomNumber(12, 90).chain( userAge => Random.of(< name: userName, age: userAge >) ) );
Одно из применение монад в чистых функциональных языках таких как Haskell — это инкапсуляция побочных эффектов. Т.к. с помощью вызовов обычных функций в таких языках нельзя обратиться к базе данных, прочитать файл или даже напечатать строку в стандартный вывод, для выполнения этих действий используются монады. В то же время эффектами их применение не ограничивается, монадный интерфейс универсален, позволяет писать обобщенный, лаконичный и высокоуровневый код, поэтому монады используются в Haskell повсеместно. За пределами Haskell применение непосредственно монад не так распространено, но их влияние отслеживается в первую очередь в программировании с промисами, а также в конструкции async-await, про которую и поговорим далее.
Async
Если вернуться к примерам кода с промисами, можно заметить, что, несмотря на преимущества промисов, цепочка вызовов выглядит немногим лучше использования коллбеков. Синтаксическая конструкция async-await позволяет пойти далее и улучшить код с цепочкой промисов, делая его похожим на код с блокирующими вызовами.
const getJson = url => fetch(url).then(resp => resp.json()); // Отправка 2-х последовательных запросов async function getUserInfo() < const userId = await getJson('/current-user-id'); const userInfo = await getJson(`/user-info/$`); console.log(`Hi $, your id is $`); >;
Возникновение async-await можно отнести к исследовательским работам по конкуррентному программированию на Haskell и ML которые подтолкнули к появлению async workflows в F# (2007) и затем C# (2011).
Выводы
Языки программирования не стоят на месте и активно развиваются, обрастая новыми средствами, всё более продвинутыми и удобными. Как видим, в последнее время в популярных языках, таких как Python или С++, стало появляться всё больше фишек, пришедших из функционального программирования. Более молодые языки, например, Scala и Kotlin, изначально создавались с поддержкой функциональных средств.
Функциональное программирование, оказывается, намного ближе, чем может показаться, даже если разработка ведётся на C++ или Java!
В комментариях будем рады услышать про ваш опыт использования этих или каких-то других функциональных фишек в повседневной разработке.
Вам может быть интересно:
- Сильные стороны функционального программирования
- Как мы выбираем языки программирования в Typeable
- А вы знаете, где сейчас используется Лисп?
- 7 полезных инструментов на Haskell
Современные языки программирования, которые заставят вас страдать: Часть 2, функциональные языки
Это продолжение перевода рейтинга современных языков программирования. В этот раз речь пойдёт о функциональных языках программирования.
Прим. ред. Это перевод статьи Ильи Суздальницкого. Мнение редакции может не совпадать с мнением автора оригинала.
Это вторая и финальная часть перевода статьи про современные язки программирования. В первой части рассматривались объектноориентированные языки. В этой части автор подробно разбирает функциональные языки программирования которые принадлежат семейству ML ( и некоторые С-подобные).
Функциональные языки программирования
Haskell
Типизация: нет более мощной системы типов, чем в Haskell. Он поддерживает, как алгебраические типы данных, так и классы типов. Язык способен вывести почти любой тип.
Сложность изучения: чтобы продуктивно использовать Haskell нужно изучить теорию категорий. Даже для того, чтобы написать hello world, нужно понимать монады.
Сообщество: больше заинтересовано в академических дискуссиях, чем в решении реальных проблем.
Функциональная чистота: чистые функции — прекрасны. Побочные эффекты (взаимодействие с внешним миром, изменение состояния) причина большого количества ошибок в программах. Будучи чисто функциональным языком, Haskell полностью избавлен от них.
Конечно в языке есть обходные пути, для взаимодействия с внешним миром. Для этого используется набор инструкций (монад ввода-вывода). Это могут быть такие инструкции: получить строку с клавиатуры, использовать её в некой функции, напечатать результат в консоль. Среда выполнения делает это за нас. Мы никогда не выполняем код взаимодействующий с внешним миром напрямую.
На практике, такая сосредоточенность на функциональной чистоте значительно увеличивает число абстракций, усложняет код и уменьшает продуктивность разработчиков.
Поддержка NULL: как и в Rust, Haskell не поддерживает нулевые ссылки. Вместо этого в нём есть Optional, на случай, если значения может не быть.
Обработка ошибок: некоторые функции могут выбрасывать ошибки, но более свойственный для языка подход похож на Result в Rust.
Иммутабельность: язык имеет первоклассную поддержку иммутабельных структур данных.
Сопоставление с образцом: поддерживается.
Экосистема: стандартная библиотека неорганизованна. По умолчанию, в Haskell используются функции выбрасывающие исключение, вместо возврата Option (золотой стандарт для функционального программирования). В довершение всего, у Haskell есть два менеджера пакетов — Cabal и Stack.
Вердикт: мне бы очень хотелось полюбить Haskell, однако он навсегда застрял в академических кругах. Является ли он худшим из функциональных языков? Решать вам, но я думаю, что это так.
OCaml
Типизация: не поддерживает классы типов, но есть функторы (модули высшего порядка). Язык статически типизирован и выводит типы почти также хорошо, как Haskell.
Экосистема: имеет небольшое сообщество и страдает от недостатка библиотек. Языку не хватает достойного веб-фреймворка. Документация хуже чем других языков.
Инструментарий: инструменты языка неорганизованны. Имеет три менеджера пакетов: Opam, Dune, и Esy. Язык известен некачественными сообщениями об ошибках компилятора. Это не критично, но снижает производительность программистов.
Обучающие ресурсы: главная книга для изучения OCaml — «Real World OCaml», не обновлялась с 2013 года. Примеры из книги устарели. Туториалов по языку недостаточно, а большая часть из них это конспекты лекций.
Параллелизм: разработчики годами ждут поддержки многоядерности, но её пока что не предвидится.
Поддержка NULL: нет нулевых ссылок, использует Option для неуказанных значений.
Обработка ошибок: нативный подход — использование типа Result.
Иммутабельность: язык имеет первоклассную поддержку иммутабельных структур данных.
Сопоставление с образцом: поддерживается.
Вердикт: Ocaml — хороший функциональный язык. Его основные недостатки: отсутствие поддержки многоядерности и небольшое сообщество (причина недостатка обучающих материалов и библиотек). Поэтому, я бы не рекомендовал язык к использованию в работе.
Scala
Экосистема: Scala — это язык из семейства Си, который выполняется на виртуальной машине Java. Это значит, что у вас есть доступ к огромной экосистеме библиотек Java.
Типизация: язык плохо справляется с приведением типов. Однако Scala поддерживает Higher-Kinded типы и типы классов.
Немногословность/читаемость: хотя программы на Scala и отличаются лаконичностью (особенно по сравнению с Java), читаемость страдает. Scala — один из немногих функциональных языков, принадлежащих к семейству Си. Си-подобные языки были предназначены для императивного программирования, а ML для функционального. Поэтому функциональный код на Scala может иногда выглядеть странно.
Синтаксис для алгебраических типов данных оставляет желать лучшего:
sealed abstract class Shape extends Product with Serializable object Shape
Этот же код на языке ReasonML:
type shape = | Square(int) | Rectangle(int, int) | Circle(int);
Скорость: hello world на языке Scala может компилироваться до 10 секунд, на слабом железе. Компиляция производится только на одном ядре процессора, что отрицательно влияет на скорость.
Из-за того, что Scala работает на виртуальной машине Java, программы запускаются дольше.
Сложность изучения: один из самых сложных функциональных языков. Scala, как и C++ обладает множеством функций, которые, однако, усложняют его изучение.
Иммутабельность: Scala обладает первоклассной поддержкой неизменяемых структур данных (с использованием классов образцов).
Поддержка NULL: с одной стороны, Scala поддерживает нулевые ссылки. С другой стороны, характерный для языка способ обработки отсутствующих значений — паттерн Option.
Обработка ошибок: нативный подход — использование типа Result.
Параллелизм: можно использовать отличный инструмент — Akka.
Сопоставление с образцом: поддерживается.
Вердикт: Scala пытается делать слишком многое. Его разработчикам пришлось пойти на множество компромиссов, чтобы поддерживать как ООП, так и функциональное программирование.
Elm
Сообщения об ошибках: компилятор выдаёт самые понятные сообщения об ошибках, что я когда-либо видел.
Обработка ошибок: в языке нет ошибок выполнения и исключений. Как и другие функциональные языки, использует тип Result для обработки ошибок.
Функциональная чистота: как и Haskell, Elm — чисто функциональный язык. И в данном случае это скорее минус, потому что любой рефакторинг превращается в кошмар.
На данный момент этот блок не поддерживается, но мы не забыли о нём! Наша команда уже занята его разработкой, он будет доступен в ближайшее время.
Elm настолько строгий, что использование табуляций считается синтаксической ошибкой.
Сосредоточенность на отсутствии ошибок убивает язык. В версии 0.19, взаимодействие с JS библиотеками сделали практически невозможным. Конечно для того, чтобы стимулировать людей писать свои библиотеки на Elm. Но компаний, у которых есть для этого достаточно ресурсов, крайне мало.
Поддержка React: Elm создаёт свою собственную виртуальную модель DOM и не использует React. Это лишает разработчиков доступа к обширной экосистеме библиотек и компонентов, созданных для React.
Состояние языка: с каждым новым релизом в языке происходят сильные изменения, которые могут лишить вас возможности использовать его.
К сожалению, прошло уже больше года с тех пор, как была выпущена новая версия Elm (0.19.1). О состоянии разработки ничего не известно. Возможно, что она вообще больше не ведётся.
Сопоставление с образцом: поддерживается.
Иммутабельность: обладает первоклассной поддержкой неизменяемых структур данных.
Поддержка NULL: нет нулевых ссылок, использует Option для неуказанных значений.
Вердикт: Elm — отличный язык, но к сожалению у него нет будущего.
F#
Типизация: единственный минус его системы типов — отсутствие Higher-Kinded типов. Тем не менее система типов очень надежна, компилятор способен вывести практически все что угодно. F# имеет надлежащую поддержку алгебраических типов данных.
Не полностью функциональный: в отличие от Haskell/Elm, F# очень прагматичен и не обеспечивает функциональную чистоту.
Обучающие ресурсы: есть действительно хорошие учебные ресурсы.
Сложность изучения: F# — один из самых простых функциональных языков.
Экосистема: имеет довольно небольшое сообщество и в отличие от таких языков как Elixir, оно не имеет таких же замечательных библиотек.
Взаимодействие с C#: F# имеет доступ ко всей экосистеме .NET/C#. Действительно хорошо взаимодействует с существующим C# кодом.
Параллелизм: работает поверх CLR, который не имеет такой же превосходной поддержки параллелизма, как Elixir на виртуальной машине Erlang.
Поддержка NULL: NULL-значения обычно не используются. Неуказанные значения обрабатываются с помощью паттерна Option.
Обработка ошибок: ошибки обрабатываются с помощью паттерна Result.
Иммутабельность: обладает первоклассной поддержкой неизменяемых структур данных.
Сопоставление с образцом: поддерживается.
Вердикт: F# — очень надежный язык программирования с действительно хорошей системой типов. Он почти так же хорош, как Elixir для разработки Web API (подробнее об этом далее). Однако проблема F# заключается не в том, что у него есть, а в том, чего у него нет. Если сравнить его с Elixir, его функционал параллелизма, богатая экосистема и удивительное сообщество перевешивают любые преимущества статической типизации, которые предоставляет F#.
Однако F# — лучший язык для финтеха. Также язык отлично подойдёт для энтерпрайз разработки. Его мощная система типов позволяет моделировать сложную бизнес логику. Очень рекомендую прочитать эту книгу — «Domain Modeling Made Functional».
ReasonML
Не является надмножеством JavaScript: синтаксис ReasonML похож на JavaScript, что делает его более доступным для всех, кто имеет опыт работы с JavaScript. Однако, в отличие от TypeScript, ReasonML даже не пытается быть надмножеством JavaScript. ReasonML не должен был унаследовать плохие дизайнерские решения, десятилетиями принимаемые в JavaScript.
Сложность изучения: ReasonML является одним из самых простых функциональных языков.
Не полностью функциональный: ReasonML очень прагматичен, ориентирован на производительность разработчиков и быстрое достижение результатов.
Типизация: его система типов почти так же хороша, как у Haskell. Самым большим недостатком является отсутствие классов типов, но он поддерживает функторы (модули высшего порядка).
ReasonML статически типизирован и выводит типы почти так же хорошо, как и Haskell.
Экосистема: как и TypeScript, ReasonML имеет доступ ко всей экосистеме JavaScript.
Взаимодействие с JavaScript/TypeScript: компилируется в обычный JavaScript. Поэтому, в одном проекте можно использовать как ReasonML, так и JavaScript/TypeScript.
ReasonML and React — отличное сочетание: поскольку ReasonML статически типизирован, нет необходимости беспокоиться о PropTypes. В отличие от JavaScript, при использовании ReasonML ничто не перерисовывается без необходимости — вы получаете отличную производительность React из коробки!
Инструменты: язык далеко не такой зрелый, как его альтернативы, так что могут возникнуть некоторые проблемы с инструментами. Например, официально рекомендуемое расширение VSCode — reason-language-server в настоящее время не работает.
ReasonML использует компилятор OCaml под капотом, а OCaml известен посредственными сообщениями об ошибках компилятора. Это не критично, но может повлиять на производительность разработчиков.
Поддержка NULL: нет нулевых ссылок, использует Option для неуказанных значений.
Иммутабельность: обладает первоклассной поддержкой неизменяемых структур данных.
Сопоставление с образцом: поддерживается.
Вердикт: это отличный язык для веб-разработки. ReasonML, вероятно, является тем, чем всегда стремился быть TypeScript, но потерпел неудачу. ReasonML добавляет статическую типизацию в JavaScript, убирая при этом все плохие фичи (и добавляя современные фичи, которые действительно нужны).
Elixir
Экосистема: это сильная сторона языка. Автор языка также разрабатывает крутые библиотеки: Phoenix и Ecto. В отличие от других языков, у Elixir нет множества библиотек с дублирующимся функционалом, а существующие — очень хороши.
Имеет хорошую документацию, даже к стандартной библиотеке.
Фреймворк Phoenix: поддерживает из коробки: вебсокеты, routing, HTML templating language, internationalization, JSON encoders/decoders, seamless ORM integration(Ecto), sessions, SPA toolkit и многое другое. Также фреймворк известен своей производительностью — способен обрабатывать миллионы одновременных подключений на одной машине.
Фуллстек Elixir: Phoenix недавно представил LiveView, который позволяет создавать насыщенные веб-интерфейсы реального времени прямо в Elixir. LiveView даже заботится о синхронизации состояния клиента и сервера, а это значит, что нам не нужно беспокоиться о разработке и обслуживании REST/GraphQL API.
Обработка данных: Elixir может быть надежной альтернативой Python для многих задач связанных с обработкой данных. Скрепер есть как у Python, так и у Elixir, и последний предлагает гораздо лучшее решение и экосистему для этой задачи.
Такие инструменты, как Broadway, позволяют строить конвейеры приема/обработки данных в Elixir.
Типизация: на мой взгляд, отсутствие корректной статической типизации — самый большой недостаток Elixir.
Скорость: компилятор Elixir является многопоточным и обеспечивает невероятно высокую скорость компиляции. В отличие от JVM, виртуальная машина Erlang запускается быстро. Производительность во время выполнения очень хороша.
Надёжность: код на Elixir выполняется поверх Erlang, который использовался более 30 лет для создания самого надежного программного обеспечения в мире. Некоторые программы, работающие на виртуальной машине Erlang, смогли достичь надежности 99,9999999%. Ни одна другая платформа в мире не может похвастаться таким же уровнем надежности.
Параллелизм: язык использует альтернативный подход к параллелизму — модель акторов. При таком подходе, у акторов (процессов) нет ничего общего. Единственный способ общения между различными процессами — отправка сообщений.
Elixir, в отличие от Go, убивает только тот процесс в котором произошла ошибка, а не всю программу. Более того, этот процесс будет автоматически перезапущен его супервизором.
Elixir строится на основе Erlang, который известен своими большими возможностями распараллеливания, и использует совершенно другой подход к параллелизму, называемый моделью актора. В рамках этой модели, между процессами (акторами) нет ничего общего. Каждый процесс поддерживает свое собственное внутреннее состояние, и единственный способ общения между различными процессами — отправка сообщений.
Процессы в Elixir очень легковесны, их можно запускать тысячами на одной машине.
Масштабирование: параллельные вычисления в Go быстрее чем в Elixir, если это происходит на одной машине. Но при масштабировании происходит обратное. Elixir легко справляется с такими вещами как: кластеризация, RPC и сетевые взаимодействия. В некотором смысле, виртуальная машина Erlang работала с микросервисами за десятилетия до того, как они вошли в обиход. Каждый процесс можно рассматривать как микросервис — как и микросервисы, процессы независимы друг от друга. Микросервисы без сложностей Kubernetes? Именно для этого и был создан Elixir.
Обработка ошибок: язык использует уникальный подход к обработке ошибок. В то время как чисто функциональные языки (Haskell/Elm) предназначены для минимизации вероятности появления ошибок, Elixir предполагает, что ошибки неизбежно произойдут.
Выбрасывать исключения в Elixir — правильно, в то время как обрабатывать исключения обычно не рекомендуется. Вместо этого супервизор процесса автоматически перезапустит неудачный процесс, чтобы программа продолжила работать.
Сложность изучения: язык можно освоить за пару месяцев. Однако освоение OTP может занять некоторое время. OTP — киллер фича языка. OTP – это набор инструментов и библиотек от Erlang, на которых строится Elixir. Это секретный ингредиент, который значительно упрощает построение параллельных и распределенных программ.
Обучающие ресурсы: их существует огромное количество. И почти все из них подойдут для новичков.
Сопоставление с образцом: поддерживается.
Вычисления: язык не справляется с задачами в которых требуется большое количество вычислений.
Вердикт: Elixir, вероятно, является самым зрелым из всех функциональных языков. Он работает на виртуальной машине, созданной для функционального программирования. Язык был разработан с нуля для параллельных вычислений, и идеально подходит для современной эры многоядерных процессоров. Это лучший язык для Web API. OTP и модель акторов делают язык лучшим решением для параллельных и распределённых программ.
Подходящий инструмент
Точно также, как не стоит пытаться забить гвоздь отвёрткой, не нужно использовать один язык программирования повсюду. Каждый из них имеет свою область применения.
Go — лучший язык для системного программирования. Для фронтенда несомненно стоит выбрать ReasonML. Абсолютный лидер для разработки Web API — Elixir. Как и для любых задач связанных с параллельными и распределёнными программами. Python, это к сожалению единственный адекватный вариант для data science.
Это довольно неоднозначная статья. Очевидно, что автор предпочитает функциональные языки программирования объектноориентированным. Если вы не согласны с рейтингом и можете аргументировать свою точку зрения, добро пожаловать в комментарии.
Модели (парадигмы) программирования
Объектно-ориентированное, структурное, обобщённое, функциональное программирование и метапрограммирование. Аспектно-ориентированное программирование, контрактное и логическое программирование. И прочие модели.
Содержание
Императивная парадигма
Императивное программирование — это парадигма программирования, в которой используются операторы, изменяющие состояние программы. Во многом так же, как повелительное наклонение в естественных языках выражает команды, императивная программа состоит из команд, которые должен выполнять компьютер. Императивное программирование фокусируется на описании того, как работает программа.
Объектно-ориентированное программирование
Структурное программирование
Структурное программирование — метод программирования, базирующийся на использовании процедурного стиля программирования и последовательной декомпозиции алгоритма решения задачи сверху вниз.
Основа для массового промышленного программирования была создана с разработкой новых методов построения программ. Одной из первых и наиболее широко применяемых технологий программирования стало структурное программирование. Этот метод до сих пор не потерял своего значения для многих классов задач.
Структурное программирование — это дисциплина, требующая, чтобы любая программа строилась из ограниченного набора типовых (базовых) управляющих структур и их композиций.
Основой структурного подхода являются два основополагающих принципа:
- использование процедурного стиля программирования;
- последовательная декомпозиция алгоритма решения задачи сверху вниз.
В соответствии с этим подходом задача решается путем выполнения следующей последовательности действий. Первоначально задача формулируется в терминах ввода данных — вывода результата: на вход программы подаются некоторые данные, программа работает и выдает ответ. После этого начинается последовательное расчленение (декомпозиция) всей задачи на отдельные более простые действия. При этом на любом этапе декомпозиции программу можно проверить, применяя механизм так называемых «заглушек» — процедур, имитирующих вход и/или выход процедур нижнего уровня. «Заглушки» позволяют проверить логику верхнего уровня до реализации следующего, т. е. на каждом шаге разработки программы существует работоспособный «каркас», который постепенно обрастает деталями.
Структурное программирование ясно определило значение модульного построения программ (т. е. разбиения монолитных программ на группу отдельных модулей) при разработке больших проектов, но в языках программирования единственным способом структуризации программы оставалось составление ее из подпрограмм и функций.
Обобщённое программирование
Обобщённое программирование — парадигма программирования, заключающаяся в таком описании данных и алгоритмов, которое можно применять к различным типам данных, не меняя само это описание. В том или ином виде поддерживается разными языками программирования. Возможности обобщённого программирования впервые появились в виде дженериков (обобщённых функций) в 1970-х годах в языках Клу и Ада, затем в виде параметрического полиморфизма в ML и его потомках, а затем во многих объектно-ориентированных языках, таких как C++, Python, Java, Object Pascal, D, Eiffel, языках для платформы .NET и других.
Аспектно-ориентированное программирование
Аспектно-ориентированное программирование — парадигма программирования, основанная на идее разделения функциональности для улучшения разбиения программы на модули.
Декларативная парадигма
Декларативное программирование — парадигма программирования, в которой задаётся спецификация решения задачи, то есть описывается ожидаемый результат, а не способ его получения. В качестве примеров декларативных языков обычно приводят HTML и SQL.
Логическое программирование
Парадигма программирования, основанная на автоматическом доказательстве теорем, а также раздел дискретной математики, изучающий принципы логического вывода информации на основе заданных фактов и правил вывода. Логическое программирование основано на теории и аппарате математической логики с использованием математических принципов резолюций
Программа здесь формулируется в виде набора логических правил, описывающих некую предметную область, а исполнение программы состоит в попытке доказать утверждение. Наиболее известным языком логического программирования можно
считать язык Пролог.
Функциональное программирование
Функциональное программирование — парадигма программирования, в которой процесс вычисления трактуется как вычисление значений функций в математическом понимании последних (в отличие от функций как подпрограмм в процедурном программировании).
Функциональное программирование предполагает обходиться вычислением результатов функций от исходных данных и результатов других функций, и не предполагает явного хранения состояния программы. Соответственно, не предполагает оно и изменяемость этого состояния.
В функциональном программировании присваивания нет вообще. Более того, чистое функциональное программирование в принципе не предполагает никаких «изменений с течением времени» и, как следствие, никаких «модифицирующих действий».
Переменные в функциональном программировании обычно всё же используются, но своё значение переменная получает при её создании и не меняет его в течение всего срока своей жизни. Чаще всего переменные используются в роли формальных параметров функций: при вызове функции такие переменные получают значения, соответствующие фактическим параметрам, и, что вполне ожидаемо, своих значений не меняют, пока работа функции не завершится — а после этого исчезают, как и все локальные сущности. Кроме того, при программировании на функциональных языках часто применяются локальные переменные, вводимые больше для удобства, чтобы поименовать значение того или иного вычисленного выражения (например, если это значение нужно использовать несколько раз).
Функциональное программирование также является ключом к некоторым языкам, популярным в определенных областях, таких как JavaScript в Интернете, R в статистике, J , K и Q в финансовом анализе, и XQuery / XSLT для XML. Декларативные языки предметной области, такие как SQL и Lex / Yacc.использовать некоторые элементы функционального программирования, такие как запрет на изменение значений. Кроме того, многие другие языки программирования поддерживают программирование в функциональном стиле или имеют реализованные функции функционального программирования, такие как C++11, C#, Kotlin, Perl, PHP, Python, Go, Rust, Raku, Scala и Java (начиная с Java 8).
Метапрограммирование
Под метапрограммированием понимается написание программного кода, результатом выполнения которого будет программный код. Наиболее очевидным вариантом здесь будет написать некую программу, которая сгенерирует и выдаст в свою очередь программу (возможно, на другом языке, а может быть, и на том же самом), и уже эта сгенерированная программа, будучи исполненной, позволит получить результаты, которые изначально были нужны. Так действительно иногда делают, хотя и сравнительно редко; намного чаще генерируется не целая программа, а какая-то её часть, например, отдельный модуль.
При этом подходе код программы не пишется вручную, а создаётся автоматически программой-генератором на основе другой программы.
Такой подход приобретает смысл, если при программировании вырабатываются различные дополнительные правила (более высокоуровневые парадигмы, выполнение требований внешних библиотек, стереотипные методы реализации определённых функций и пр.). При этом часть кода (или данных) теряет содержательный смысл и становится лишь механическим выполнением правил. Когда эта часть становится значительной, возникает мысль задавать вручную лишь содержательную часть, а остальное добавлять автоматически. Это и проделывает генератор.
Различаются два принципиально различных вида кодогенерации:
- генератор является физически отдельной бинарной программой, необязательно написанной на целевом языке.
- целевой язык является одновременно языком реализации генератора, так что метапрограмма составляет с целевой программой единое целое.
Контрактное программирование
Метод проектирования программного обеспечения. Он предполагает, что проектировщик должен определить формальные, точные и верифицируемые спецификации интерфейсов для компонентов системы. При этом, кроме обычного определения абстрактных типов данных, также используются предусловия, постусловия и инварианты. Данные спецификации называются «контрактами» в соответствии с концептуальной метафорой условий и ответственности в гражданско-правовых договорах.
Основная идея контрактного программирования — это модель взаимодействия элементов программной системы, основывающаяся на идее взаимных обязательств и преимуществ. Как и в бизнесе, клиент и поставщик действуют в соответствии с определённым контрактом. Контракт некоторого метода или функции может включать в себя:
- конкретные обязательства, которые любой клиентский модуль должен выполнить перед вызовом метода — предусловия, которые дают преимущество для поставщика — он может не проверять выполнение предусловий;
- конкретные свойства, которые должны присутствовать после выполнения метода — постусловия, которые входят в обязательства поставщика;
- обязательства по выполнению конкретных свойств — инвариантов, которые должны выполняться при получении поставщиком сообщения, а также при выходе из метода.
Многие языки программирования позволяют учитывать такие обязательства. Контрактное программирование подразумевает эти требования критическими для корректности программ, поэтому они должны быть утверждены при проектировании. Таким образом, контрактное программирование предписывает начинать писать код с написания формальных утверждений корректности (assertions).
Условия, облегчающие применение контрактного программирования:
- наличие наследования и возможность динамического связывания,
- способность обрабатывать исключения,
- возможность автоматического документирования ПО.
- Голицына О. Л., Партыка Т. Л., Попов И. И. Языки программирования : учебное пособие / О. Л. Голицына, Т. Л. Партыка, И. И. Попов. — 2-е изд., перераб. и доп. — М. : ФОРУМ, 2010. — 400 с.
- Свердлов С. 3. Языки программирования и методы трансляции: Учебное пособие. — СПб.: Питер, 2007. — 638 с.
- Столяров А. В. Программирование: введение в профессию. — Изд. 2-е, испр. и доп. : в 3 томах / А. В. Столяров. — Москва : МАКС Пресс, 2021. — Том III : Парадигмы. — 704 с.

27.01.2022, 546 просмотров.