Функциональные языки программирования
Функциональные языки программирования — это языки, в которых процессы представлены как функции в математическом понимании этого слова. То есть функция в них определяется не как подпрограмма, а как соответствие между множествами.
«IT-специалист с нуля» наш лучший курс для старта в IT
Такой подход к программированию называют функциональным. Название не значит, что код основан на функциях: это справедливо почти для любого языка. Функциональность определяется именно подходом: весь код описывается как правила работы с информацией, и они могут исполняться в любом порядке.
Функциональный подход — противоположность императивному, в котором программист задает программе четкий порядок действий по шагам. Тут все сложнее: программа сама решает, как и в каком порядке исполнять действия, а программист описывает правила взаимодействия и связи между компонентами.
Кто и где пользуется функциональным программированием
Сейчас программисты чаще всего работают с ООП — объектно-ориентированным программированием. Функциональный подход используют реже: он сложнее и применим не ко всем задачам. Но сейчас его популярность растет, и тому есть причины: чистота кода, надежность программ, высокие возможности для оптимизации.
Благодаря своим особенностям функциональное программирование распространено при работе с важными данными или при решении задач, где нужны сложные вычисления. Есть фреймворки, с которыми проще работать в функциональном стиле, а есть такие, в которых сочетаются оба подхода. Как пример — React и Redux для JavaScript.
Сейчас программисты придерживаются такого подхода: полностью писать какой-то проект в функциональном стиле не стоит, но его нужно держать в голове и использовать при решении конкретных задач. Эта идея актуальна для большинства направлений разработки.
Профессия / 8 месяцев
IT-специалист с нуля
Попробуйте 9 профессий за 2 месяца и выберите подходящую вам
Какие языки программирования функциональные
Условно можно выделить две группы. Первая — языки, жестко ориентированные на функциональное программирование. Вторая — так называемые мультипарадигменные языки, то есть такие, на которых можно писать по-разному. В том числе в функциональном стиле.
К функциональным языкам относятся Haskell, F#, OCaml, ELM, серия языков Lisp, а также Erlang и его потомок Elixir. Иногда сюда же относят Scala и Nemerle, хотя эти языки дают возможность программировать и в функциональном, и в императивном стилях. Они старые и сейчас применяются не так часто, как большинство современных.
Еще к этой группе относится несколько узкоспециализированных языков: язык вычислительной платформы Wolfram, языки J и K для финансового анализа, язык R для статистических целей. На функциональном языке APL базируется язык научной математической среды MATLAB. Также сюда можно отнести языки, которые используются при работе с электронными таблицами, а еще, частично, SQL — язык запросов для работы с базами данных.
К мультипарадигменным языкам, на которых можно писать в функциональном стиле, относятся уже упомянутые Scala и Nemerle, а также Go, JavaScript и некоторые его фреймворки. В меньшей степени сюда же можно отнести Python, Ruby, PHP и C++, а также Java: они больше ориентированы на ООП, но в них есть и функциональные элементы.
Разница между функциональным и императивным подходом
Чтобы лучше понять, как это работает, воспользуемся аналогией.
- Императивная парадигма похожа на правила умножения в столбик. Последовательность действий, их порядок и тип четко определены. Мы выполняем команды, которые кто-то придумал до нас, как по инструкции.
- Функциональная парадигма — это, скорее, правила орфографии и пунктуации. Нет четкой последовательности, как именно их применять. Правила нельзя представить как строгую инструкцию. Вместо этого мы сами решаем, какое правило в какой ситуации будет актуально. И последовательность, в которой мы это делаем, не имеет значения.
Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить
В чем разница с ООП
Объектно-ориентированное программирование отличается от функциональной парадигмы. В нем все представлено в виде объектов, в функциональном — в виде функций. ООП смешивает данные и поведение, функциональный подход — разделяет. Различаются особенности работы с информацией, структура программ и многое другое.
ООП в целом относится скорее к императивному типу программирования: код — это набор команд, рассказывающих компьютеру, что делать. Но «чистым» императивным программированием его назвать сложно — скорее, дополненным и измененным.
Сложность изучения функциональных языков
О функциональном программировании можно услышать, что оно сложно в освоении. Но тут есть парадокс, о котором говорят некоторые программисты: новичку понять его принципы может быть легче, чем разработчику с опытом программирования в ООП. Это связано с тем, что разработчики в императивных стилях уже привыкли к определенному типу логики, а перестроиться на что-то принципиально новое сложнее, чем изучать с нуля.
Сейчас считается, что хороший разработчик должен разбираться в обеих парадигмах и знать, когда лучше применять одну, а когда — другую.
Особенности функционального подхода
Функциональное программирование определяется несколькими важными правилами. Это основы, которые нужно знать, чтобы представлять, как в принципе работает парадигма.
Отсутствие жесткой последовательности. Об этом мы уже говорили. Разработчик задает правила, а компилятор кода сам решает, в какой последовательности их выполнять. Жесткий порядок действий программист не задает. Его выбирает сама программа.
«Чистые» функции. Чистые функции — это такие, которые удовлетворяют двум условиям:
- при одинаковых входных данных функция всегда вернет одинаковый результат. То есть, функция, возвращающая сумму a и b, может быть чистой, а функция, возвращающая случайное число, — нет;
- когда функция выполняется, не возникают побочные эффекты — так называют действия, которые влияют на что-то за ее пределами. Например, изменение переменной, чтение данных или вывод в консоль — это побочные эффекты.
В функциональном программировании все функции должны быть чистыми. Кажется, будто это сложно и ограничивает разработчика, но на самом деле при грамотном подходе такое даже расширяет возможности. Ведь чистые функции можно запускать, не боясь, что они что-то изменят или нарушат.
Неизменные переменные. В функциональном программировании нет переменных в привычном виде. В нем все объявленные переменные неизменны — то есть фактически это константы. Если с какой-то переменной нужно провести вычисления, она не изменяется: создается новая переменная, и результат вычислений записывается в нее. А исходная остается прежней — ее значение не меняется.
«Первоклассные» функции высшего порядка. Все функции в функциональном программировании должны быть первого класса и высшего порядка. Сейчас объясним, что это значит.
- Функция первого класса — это такая, которую можно представить как переменную. То есть, ее можно передавать как аргумент другим функциям, возвращать как результат работы других функций, сохранять в переменную или структуру данных.
- Функция высшего порядка — такая, которая принимает в качестве аргументов функции или возвращает их в качестве результата.
Возможность работы с такими функциями есть не только в функциональном программировании. Более того, такое требование есть не только в нем. Но для него такой подход обязателен — вместе с другими особенностями.
Относительная прозрачность. Еще одно требование к функциям в функциональном программировании — относительная прозрачность. Это понятие может быть сложным для понимания, но мы постараемся его объяснить. Относительная прозрачность означает, что выражение, которое возвращает функция, можно заменить значением — и от этого ничего не изменится. То есть, если функция, например, складывает два числа 3 и 5, то она вернет сумму 3 + 5. Теоретически вместо этой функции в выражение можно подставить число 8, и от этого программа не изменится — она будет работать так же.
Это не означает, что функция должна выдавать одинаковый результат во всех случаях — только при одинаковых входных данных. Про это также говорит часть определения чистой функции.
Рекурсия вместо циклов. В классическом функциональном программировании циклы реализованы как рекурсия. Стоит понимать разницу:
- цикл — несколько выполнений одной и той же части кода подряд;
- рекурсия — явление, когда функция вызывает сама себя, но с другими аргументами.
Очень многие алгоритмы в функциональном подходе построены на рекурсии — функциях, вызывающих себя. Так реализованы многие действия, где что-то нужно выполнить несколько раз.
Лямбда-исчисление. Это особая математическая система, которая используется в функциональных языках программирования. Ее название также иногда пишут как λ-исчисление. Мы не будем углубляться в сложные математические понятия и выделим только несколько особенностей, важных для понимания функционального программирования:
- в λ-исчислении есть две основных операции — аппликация и абстракция. Первое — это, по сути, вызов функции к заданному значению. Второе — построение функции по имеющимся выражениям;
- все функции могут быть анонимными и складываться только из списка аргументов. Анонимные функции — это такие, у которых нет уникального имени, а объявляются они в месте выполнения;
- при вызове функции с несколькими аргументами происходит ее каррирование — преобразование в несколько функций, в каждой из которых один аргумент. То есть, функция вида f(a, b, c) превратится в набор функций f(a)(b)(c). Результатом f(a) будет функция, которая тут же применится к аргументу b. И так далее. Это рекурсия — та, о которой мы говорили выше.
Курс для новичков «IT-специалист
с нуля» – разберемся, какая профессия вам подходит, и поможем вам ее освоить
Преимущества функциональных языков
Чистота кода. Код, написанный на функциональном языке, выглядит чистым и понятным. Сюда же можно отнести локальную читаемость — можно разобраться, как работает та или иная функция, без строгой привязки к остальному коду. Код более чистый еще и за счет отсутствия четкой последовательности: чтобы понять происходящее в нем, не обязательно знать порядок выполнения разных действий.
Надежность. Благодаря тому, что функции чистые и не изменяют окружение вокруг себя, функциональный код более надежен. Если в одной конкретной функции что-то сломается, это не повлечет за собой проблемы с другими компонентами. Не нужно отслеживать побочные эффекты — согласно определению чистой функции их быть просто не должно. Правда, на практике это не всегда возможно, но эту деталь мы подробнее обсудим ниже.
Оптимизация. Когда компилятор обрабатывает функциональную программу, он сам решает, в каком порядке вызывать функции. За счет этого программы легче оптимизировать: такой подход открывает широкие возможности для автоматической оптимизации на уровне компилятора. Оптимизация означает, что код будет быстрее или производительнее.
Удобное тестирование. Благодаря все тем же чистым функциям этот стиль программирования удобнее отлаживать и тестировать. Особенно это заметно при модульном тестировании — таком, где каждый компонент проверяется по отдельности. Ведь если мы проверяем функцию, которая не изменяет ничего снаружи — значит, нам не нужно дополнительно думать о тестировании возможных побочных эффектов.
Распараллеливание вычислений. За счет отсутствия жесткой последовательности функциональное программирование отлично подходит для параллельных вычислений — одновременного выполнения нескольких действий. С императивным подходом их сложнее организовать, кроме того, нужно учитывать побочные эффекты. А функциональное программирование гарантирует, что вызов одной функции не повлияет на вызов другой — поэтому снижается риск ошибок и конфликтов при параллельных вычислениях.
Гибкая работа с функциями. Благодаря гибкой и сложной работе с функциями некоторые действия можно выполнять быстрее и удобнее, чем с императивным подходом. Это мощный инструмент, особенно для решения специфических задач: математических, научных, связанных с точными вычислениями или подобными сферами. Популярность подхода при решении таких задач видно и на практике: языки для математических, научных, экономических или статистических расчетов — по большей части функциональные.
Недостатки функциональных языков
Использование большого объема памяти. Этот минус вытекает из тех же особенностей, что и преимущества. Многие действия построены на рекурсии, а при изменении любого значения создается новая переменная — поэтому программа начинает требовать больше памяти, чем императивная с классическими циклами и изменяемыми значениями. Это значит, что для эффективной работы в языке должен быть мощный сборщик мусора или удобные инструменты для ручной работы с памятью. За ней нужно следить, иначе есть риск серьезного снижения производительности.
Непредсказуемый порядок действий. Эта особенность функционального программирования — плюс и минус одновременно. О плюсах мы уже говорили выше. Минус в том, что для некоторых важных задач порядок действий важен по определению. Например, ввод и вывод. Если данные будут вводиться или выводиться хаотично, в непредсказуемом порядке, это ухудшит работу программы. Поэтому часто функциональное программирование комбинируют с императивным — для большей гибкости и производительности кода в целом.
Неуниверсальность чистых функций. Одними чистыми функциями не получится решить многие задачи. Некоторые важные действия по определению сложно или невозможно реализовать через чистые функции. Поэтому программистам приходится прибегать к дополнительным ухищрениям и усложнять код, чтобы избежать этого минуса. Также некоторые функции на практике оказываются не совсем чистыми — тут опять же приходится обходить ограничения и придумывать новые способы.
Как начать программировать в функциональном стиле
Сначала вам понадобится познакомиться с основами парадигмы и с теорией. Можно скомбинировать это с началом изучения функциональных языков, чтобы сразу «пощупать» подход на практике. Но помните, что многие решения сначала могут показаться вам неочевидными — к особым принципам нужно привыкнуть. Тем не менее, функциональное программирование – мощный и интересный инструмент, и изучить его вполне реально даже новичку.
IT-специалист с нуля
Наш лучший курс для старта в IT. За 2 месяца вы пробуете себя в девяти разных профессиях: мобильной и веб-разработке, тестировании, аналитике и даже Data Science — выберите подходящую и сразу освойте ее.
Функциональное программирование и примеры его использования
Функциональное программирование — одна из самых популярных парадигм программирования, которая все больше и больше привлекает внимание разработчиков со всего мира. Это подход, который основан на математических функциях и их комбинациях для решения задач. Но что такое функциональное программирование и почему оно так важно для современной разработки программного обеспечения? В этой статье мы разберемся в основных принципах функционального программирования, рассмотрим популярные функциональные языки программирования, и узнаем, как функциональное программирование помогает разработчикам создавать более надежные и устойчивые приложения.
Определение понятия «функциональное программирование»:
Функциональное программирование — это парадигма программирования, основанная на использовании функций в качестве основного строительного блока программы.
В функциональном программировании данные считаются неизменяемыми, а функции — чистыми, то есть не имеющими побочных эффектов и всегда возвращающими одинаковый результат для заданных входных параметров. Функциональное программирование позволяет создавать более надежные и устойчивые программы, которые проще тестировать и поддерживать.
Особенности функционального программирования
Основные особенности функционального программирования включают в себя использование чистых функций, неизменяемых данных, рекурсии и ленивых вычислений. Функциональное программирование также поддерживает композицию функций и работу с функциями высшего порядка. Благодаря этим особенностям оно обладает рядом преимуществ перед другими парадигмами программирования, такими как повышенная надежность и устойчивость программ, улучшенная параллелизация и возможность создания более читаемого и модульного кода.
Функциональное программирование также обладает многими другими особенностями, такими как высокий уровень абстракции, лаконичность и простота синтаксиса, возможность параллельного выполнения кода и т.д. Все эти особенности делают функциональное программирование мощным и гибким инструментом для разработки программного обеспечения.
Основные концепции функционального программирования
Основные концепции функционального программирования представляют собой набор методов и принципов, которые позволяют создавать более чистые, гибкие и масштабируемые программы. Использование этих концепций помогает улучшить качество кода и упростить процесс разработки программных приложений. Перечислим основные из них:
- Чистые функции — это функции, которые не имеют побочных эффектов и всегда возвращают одинаковый результат для заданных входных параметров. Такие функции более надежны и легче тестировать.
- Неизменяемость данных — в функциональном программировании данные считаются неизменяемыми, и изменение данных происходит путем создания новых объектов. Это позволяет избежать ошибок, связанных с изменением одних и тех же данных разными частями программы.
- Рекурсия— в функциональном программировании используются функции, которые вызывают сами себя. Рекурсия позволяет описывать сложные операции, такие как обход деревьев или вычисление факториала, более просто и элегантно.
- Функциональные типы данных — в функциональном программировании типы данных определяются как множества значений, а не наборы операций. Такие типы данных обеспечивают более высокую степень абстракции и обобщения, что упрощает написание кода.
- Композиция функций позволяет создавать новые функции из уже существующих, что является одним из основных принципов функционального программирования — повторное использование кода.
- Ленивые вычисления — это еще одна концепция, которая позволяет вычислять значение только в тот момент, когда оно действительно необходимо для выполнения программы, что уменьшает потребление ресурсов.
- Currying — это еще один важный концепт функционального программирования, который позволяет легко комбинировать функции и создавать новые функции с помощью уже существующих.
Популярные функциональные языки программирования
Функциональные языки программирования — это те языки, которые основываются на функциональном программировании. Популярность функциональных языков программирования объясняется их способностью создавать код, который проще поддается анализу, тестированию и оптимизации. Кроме того, они позволяют программистам писать программы более декларативно, что может привести к повышению производительности и сокращению времени разработки. Также функциональные языки программирования могут использоваться для создания параллельных и распределенных систем, что актуально в наше время. Перечислим основные:
- Haskell — чистый функциональный язык программирования, который используется для разработки сложных систем и алгоритмов. Он популярен благодаря своей выразительности, мощности и безопасности. Haskell обладает множеством возможностей для абстракции и композиции функций, а также для ленивых вычислений.
- Clojure — динамический функциональный язык программирования, работающий на платформе Java. Он популярен благодаря своей простоте и элегантности. Clojure поддерживает неизменяемость данных, функции высшего порядка, ленивые вычисления и имеет мощные механизмы для работы с последовательностями данных.
- Scala — объектно-ориентированный и функциональный язык программирования, который работает на платформе Java. Scala популярен благодаря своей гибкости и возможности использования как функционального, так и объектно-ориентированного стиля программирования. Он имеет мощную систему типов, поддерживает ленивые вычисления и работу с неизменяемыми данными.
- F# — функциональный язык программирования, разработанный компанией Microsoft для платформы .NET. Он сочетает возможности функционального и объектно-ориентированного программирования и использует сильную статическую типизацию.
- Erlang — функциональный язык программирования, изначально разработанный для создания распределенных систем. Он поддерживает параллельное и распределенное программирование, обладает высокой отказоустойчивостью и масштабируемостью.
- OCaml — функциональный язык программирования, использующий статическую типизацию и поддерживающий объектно-ориентированное программирование. Он широко используется в академических кругах и в индустрии.
- Lisp — один из старейших функциональных языков программирования, который был разработан в 1958 году. Lisp используется для разработки искусственного интеллекта, компьютерной лингвистики и других областей.
Помимо прочего, данные языки программирования имеют развитые экосистемы, включающие библиотеки и инструменты для разработки.
Преимущества и недостатки функционального программирования
В функциональном программировании основной упор делается на то, как программировать, а не на то, что программировать. Это означает, что в функциональном программировании код пишется в терминах функций, а не в терминах последовательности инструкций. Такой подход дает ряд преимуществ, но и имеет некоторые ограничения и недостатки.
Преимущества | Недостатки |
Упрощение разработки | Сложность обучения |
Повышение надежности | Ограничения в работе с изменяемыми данными |
Легкость тестирования | Трудность оптимизации производительности |
Высокая скорость работы | Сложность работы с побочными эффектами |
Масштабируемость | Не подходит для всех типов задач и проектов |
Поддержка параллелизма | Необходимость использования специальных инструментов для решения определенных задач |
Указанные в таблице преимущества и недостатки не относятся к каждому языку программирования, который применяет функциональный подход, но они представляют общие идеи, связанные с функциональным программированием в целом.
Примеры применения функционального программирования
Концепции функционального программирования давно используются в различных сферах и индустриях и привели ко множеству успехов. Вот некоторые из примеров:
- Язык программирования Haskell используется в финансовой индустрии для создания безопасных и надежных финансовых приложений. Например, банк Standard Chartered использует Haskell для создания своей системы обработки транзакций.
- Spotify использует функциональное программирование в своей системе потоковой передачи музыки. Они используют язык программирования Erlang и его фреймворк OTP (Open Telecom Platform) для создания высокоэффективной и отказоустойчивой системы.
- Компания Jane Street Capital, занимающаяся торговлей на финансовых рынках, использует OCaml для создания высокопроизводительных и безопасных торговых систем.
- Язык программирования Clojure используется в бэкэнде различных веб-сервисов, таких, как Amazon Web Services и Walmart.
- Еще пример применения функционального программирования в реальном мире — часть бэкенда Facebook, которая была написана на Haskell, а также в модуле онлайн-спам-фильтра применялась смесь Haskell/C++, обрабатывающая до миллиона сообщений в секунду.
Все эти примеры демонстрируют, что функциональное программирование может быть эффективным и надежным решением для создания сложных систем в различных областях применения.
Заключение
Функциональное программирование является важной парадигмой программирования, которая приобретает все большую популярность. Преимущества функционального программирования, такие как упрощение разработки, повышение надежности и легкость тестирования, делают его привлекательным выбором для многих проектов (что демонстрируют реальные примеры проектов). Однако, функциональное программирование также имеет свои ограничения и недостатки, и должно быть применено с учетом контекста конкретного проекта.
Что такое функциональное программирование?
В этой статье Владимир Хориков попытается ответить на вопрос: что такое функциональное программирование?
Функциональное программирование
Итак, что такое функциональное программирование? Этот термин возникает довольно часто, и каждый автор, пишущий о нем, дает собственное объяснение. На взгляд автора оригинала, самым простым и в то же время точным определением является следующее: функциональное программирование — это программирование с математическими функциями.
Математические функции не являются методами в программном смысле. Хотя мы иногда используем слова «метод» и «функция» как синонимы, с точки зрения функционального программирования это разные понятия. Математическую функцию лучше всего рассматривать как канал (pipe), преобразующий любое значение, которое мы передаем, в другое значение:
Вот и все. Математическая функция не оставляет во внешнем мире никаких следов своего существования. Она делает только одно: находит соответствующий объект для каждого объекта, который мы ему скармливаем.
Для того чтобы метод стал математической функцией, он должен соответствовать двум требованиям. Прежде всего, он должен быть ссылочно прозрачным (referentially transparent). Ссылочно прозрачная функция всегда дает один и тот же результат, если вы предоставляете ей одни и те же аргументы. Это означает, что такая функция должна работать только со значениями, которые мы передаем, она не должна ссылаться на глобальное состояние.
public long TicksElapsedFrom(int year)
Этот метод не является ссылочно прозрачным, потому что он возвращает разные результаты, даже если мы передаем в него один и тот же год. Причина здесь в том, что он ссылается на глобальное свойство DatetTime.Now.
Ссылочно прозрачной альтернативой этому методу может быть (Эта версия работает только с переданными параметрами):
public long TicksElapsedFrom(int year, DateTime now)
Во-вторых, сигнатура математической функции должна передавать всю информацию о возможных входных значениях, которые она принимает, и о возможных результатах, которые она может дать. Можно называть эту черту честность сигнатуры метода (method signature honesty).
Посмотрите на этот пример кода:
public int Divide(int x, int y) < return x / y; >
Метод Divide, несмотря на то, что он ссылочно прозрачный, не является математической функцией. В его сигнатуре указано, что он принимает любые два целых числа и возвращает другое целое число. Но что произойдет, если мы передадим ему 1 и 0 в качестве входных параметров?
Вместо того, чтобы вернуть целое число, как мы ожидали, он вызовет исключение «Divide By Zero». Это означает, что сигнатура метода не передает достаточно информации о результате операции. Он обманывает вызывающего, делая вид, что может обрабатывать любые два параметра целочисленного типа, тогда как на практике он имеет особый случай, который не может быть обработан.
Чтобы преобразовать метод в математическую функцию, нам нужно изменить тип параметра «y», например:
public static int Divide(int x, NonZeroInteger y) < return x / y.Value; >
Здесь NonZeroInteger — это пользовательский тип, который может содержать любое целое число, кроме нуля. Таким образом, мы сделали метод честным, поскольку теперь он не ведет себя неожиданно для любых значений из входного диапазона. Другой вариант — изменить его возвращаемый тип:
public static int ? Divide(int x, int y) < if (y == 0) return null; return x / y; >
Эта версия также честна, поскольку теперь не гарантирует, что она вернет целое число для любой возможной комбинации входных значений.
Несмотря на простоту определения функционального программирования, оно включает в себя множество приемов, которые многим программистам могут показаться новыми. Посмотрим, что они из себя представляют.
Побочные эффекты (Side effects)
Первая такая практика — максимально избегать побочных эффектов за счет использования иммутабельности по всей базе кода. Этот метод важен, потому что акт изменения состояния противоречит функциональным принципам.
Сигнатура метода с побочным эффектом не передает достаточно информации о фактическом результате операции. Чтобы проверить свои предположения относительно кода, который вы пишете, вам нужно не только взглянуть на саму сигнатуру метода, но также необходимо перейти к деталям его реализации и посмотреть, оставляет ли этот метод какие-либо побочные эффекты, которых вы не ожидали:
В целом, код со структурами данных, которые меняются со временем, сложнее отлаживать и более подвержен ошибкам. Это создает еще больше проблем в многопоточных приложениях, где у вас могут возникнуть всевозможные неприятные условия гонки.
Когда вы работаете только с иммутабельными данными, вы заставляете себя обнаруживать скрытые побочные эффекты, указывая их в сигнатуре метода и тем самым делая его честным. Это делает код более читабельным, потому что вам не нужно останавливаться на деталях реализации методов, чтобы понять ход выполнения программы. С иммутабельными классами вы можете просто взглянуть на сигнатуру метода и сразу же получить хорошее представление о том, что происходит, без особых усилий.
Исключения
Исключения — еще один источник нечестности для вашей кодовой базы. Методы, которые используют исключения для управления потоком программы, не являются математическими функциями, потому что, как и побочные эффекты, исключения скрывают фактический результат операции.
Более того, исключения имеют семантику goto, что означает, что они позволяют легко переходить из любой точки вашей программы в блок catch. На самом деле, исключения работают еще хуже, потому что оператор goto не позволяет выходить за пределы определенного метода, тогда как с исключениями вы можете легко пересекать несколько уровней в своей базе кода.
Примитивная одержимость (Primitive Obsession)
В то время как побочные эффекты и исключения делают ваши методы нечестными в отношении их результатов, примитивная одержимость вводит читателя в заблуждение относительно входных значений методов. Вот пример:
public class User < public string Email < get; private set; >public User(string email) < if (email.Length >256) throw new ArgumentException("Email is too long"); if (!email.Contains("@")) throw new ArgumentException("Email is invalid"); Email = email; > > public class UserFactory < public User CreateUser(string email) < return new User(email); >>
Что нам говорит сигнатура метода CreateUser? Она говорит, что для любой входной строки он возвращает экземпляр User. Однако на практике он принимает только строки, отформатированные определенным образом, и выдает исключения, если это не так. Следовательно, этот метод нечестен, поскольку не передает достаточно информации о типах строк, с которыми работает.
По сути, это та же проблема, которую вы видели с методом Divide:
public int Divide(int x, int y) < return x / y; >
Тип параметра для электронной почты, а также тип параметра для «y» являются более грубыми, чем фактическая концепция, которую они представляют. Количество состояний, в которых может находиться экземпляр строкового типа, превышает количество допустимых состояний для правильно отформатированного электронного письма. Это несоответствие приводит к обману разработчика, который использует такой метод. Это заставляет программиста думать, что метод работает с примитивными строками, тогда как на самом деле эта строка представляет концепцию предметной области со своими инвариантами.
Как и в случае с методом Divide, нечестность можно исправить, введя отдельный класс Email и используя его вместо строки.
Nulls
Еще одна практика в этом списке — избегать nulls. Оказывается, использование значений NULL делает ваш код нечестным, поскольку сигнатура методов, использующих их, не сообщает всю информацию о возможном результате соответствующей операции.
Но тут, конечно, зависит от языка. Автор оригинала работает с C#, в котором до 8 версии нельзя было указывать является ли значение nullable (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types). Так как оригинал статьи 2016 года, на тот момент еще не было такой возможности в C#.
Фактически, в C # все ссылочные типы действуют как контейнер для двух типов значений. Один из них является экземпляром объявленного типа, а другой — null. И нет никакого способа провести различие между ними, поскольку эта функциональность встроена в сам язык. Вы всегда должны помнить, что, объявляя переменную ссылочного типа, вы фактически объявляете переменную пользовательского двойного типа, которая может содержать либо нулевую ссылку, либо фактический экземпляр:
В некоторых случаях это именно то, что вам нужно, но иногда вы хотите просто вернуть MyClass без возможности его преобразования в null. Проблема в том, что в C # это невозможно сделать. Невозможно различить ссылочные типы, допускающие значение NULL, и ссылочные типы, не допускающие значения NULL. Это означает, что методы со ссылочными типами в своей сигнатуре по своей сути нечестны.
Эту проблему можно решить, введя тип Maybe и соглашение внутри команды о том, что всякий раз, когда вы определяете переменную, допускающую значение NULL, вы используете для этого тип Maybe.
Почему функциональное программирование?
Важный вопрос, который приходит на ум, когда вы читаете о функциональном программировании: зачем вообще беспокоиться об этом?
Одной из самых больших проблем, возникающих при разработке корпоративного программного обеспечения, является сложность. Сложность кодовой базы, над которой мы работаем, является единственным наиболее важным фактором, влияющим на такие вещи, как скорость разработки, количество ошибок и способность быстро приспосабливаться к постоянно меняющимся потребностям рынка.
Существует некий предел сложности, с которой мы можем справиться за раз. Если кодовая база проекта превышает этот предел, становится действительно трудно, а в какой-то момент даже невозможно что-либо изменить в программном обеспечении без каких-либо неожиданных побочных эффектов.
Применение принципов функционального программирования помогает снизить сложность кода. Оказывается, программирование с использованием математических функций значительно упрощает нашу работу. Благодаря двум характеристикам, которыми они обладают — честности сигнатуры метода и ссылочной прозрачности — мы можем гораздо проще понимать и рассуждать о таком коде.
Каждый метод в нашей кодовой базе — если он написан как математическая функция — можно рассматривать отдельно от других. Когда мы уверены, что наши методы не влияют на глобальное состояние или не работают с исключением, мы можем рассматривать их как строительные блоки и компоновать их так, как мы хотим. Это, в свою очередь, открывает большие возможности для создания сложной функциональности, которую создать ненамного сложнее, чем части, из которых она состоит.
Имея честную сигнатуру метода, нам не нужно останавливаться на деталях реализации метода или обращаться к документации, чтобы узнать, есть ли что-то еще, что нам нужно учесть перед его использованием. Сама сигнатура сообщает нам, что может случиться после того, как мы вызовем такой метод. Модульное тестирование также становится намного проще. Все сводится к паре строк, в которых вы просто указываете входное значение и проверяете результат. Нет необходимости создавать сложные тестовые двойники, такие как mocks, и поддерживать их в дальнейшем.
Резюме
Функциональное программирование — это программирование с использованием математических функций. Для преобразования методов в математические функции нам нужно сделать их сигнатуры честными в том смысле, что они должны полностью отражать все возможные входные данные и результаты, и нам также необходимо убедиться, что метод работает только с теми значениями, которые мы передаем, и ничего больше.
Практики, которые помогают преобразовать методы в математические функции:
- Иммутабельность.
- Избегать исключения для управления потоком программы.
- Избавляться от примитивной одержимости.
- Делать nulls явными.
- программирование
- функциональное программирование
- проектирование
Знакомство с функциональным программированием в Python, JavaScript и Java
Функциональное программирование (ФП) представляет собой процесс создания ПО путем компоновки чистых функций. В современном мире работодатели ищут программистов, способных применять к решению задач различные парадигмы программирования. При этом наблюдается рост популярности именно функциональной, так как она очень эффективна и позволяет легко масштабировать проекты.
Как же можно половчее переключиться от ООП к ФП?
Сегодня мы изучим ключевые принципы функционального программирования, рассмотрим их реализацию в Python, JavaScript и Java, а также прикинем, в каком направлении лучше всего продолжать двигаться.
По ходу статьи мы ответим на следующие вопросы:
- Что такое функциональное программирование?
- Как оно реализовано в различных языках?
- Каковы его основные принципы?
- Как оно используется в Python, JavaScript и Java?
- Что стоит изучать далее?
Что такое функциональное программирование?
Функциональное программирование — это парадигма декларативного программирования, в которой программы создаются путем последовательного применения функций, а не инструкций.
Каждая из этих функций принимает входное значение и возвращает согласующееся с ним выходное значение, не изменяясь и не подвергаясь воздействию со стороны состояния программы.
Для таких функций предусмотрено выполнение только одной операции, если же требуется реализовать сложный процесс, то используется уже композиция функций, связанных последовательно. В процессе ФП мы создаем код, состоящий из множества модулей, поскольку функции в нем могут повторно использоваться в разных частях программы путем вызова, передачи в качестве параметров или возвращения.
Чистые функции не производят побочных эффектов и не зависят от глобальных переменных или состояний.
Функциональное программирование используется, когда решения легко выражаются с помощью функций и не имеют ощутимой связи с физическим миром. В то время как объектно-ориентированные программы моделируют код по образцу реальных объектов, ФП задействует математические функции, в которых промежуточные или конечные значения не сопоставляются с объектами физического мира.
К наиболее распространенным областям, применяющим ФП, относятся проектирование ИИ, алгоритмы классификации в МО, финансовые программы, а также продвинутые модели математических функций.
Проще говоря: функциональные программы выполняют много чистых однозадачных функций, совмещенных в последовательность для решения сложных математических или не связанных с физическим миром задач.
Преимущества функционального программирования
- Легкая отладка: чистые функции и неизменяемые данные упрощают обнаружение мест определения значений переменных. В чистых функциях меньше факторов, влияющих на них, что позволяет быстрее находить проблемные участки кода.
- Отложенное вычисление: функциональные программы производят вычисления только при необходимости. Это позволяет им повторно использовать ранее полученные результаты и экономить время на выполнение.
- Модульность: чистые функции не полагаются на внешние переменные или состояния, в связи с чем их можно легко переиспользовать в разных местах программы. Кроме того, функции будут выполнять только одну операцию или вычисление, что не позволит вам при их использовании случайно импортировать лишний код.
- Лучшая читаемость: функциональные программы легко читать, потому что поведение каждой функции неизменяемо и изолировано от состояния программы. В результате вы зачастую можете легко понять, что будет делать функция, просто по ее имени.
- Параллельное программирование: программы легче создавать при помощи функционального подхода, потому что неизменяемые переменные снижают число изменений внутри этих программ. Каждой функции приходится работать только с вводом пользователя, и она может быть уверена, что состояние программы в основном останется прежним.
Языки функционального программирования
Функциональная парадигма поддерживается не во всех языках. Некоторые из них, например Haskell, спроектированы именно для этой задачи, в то время как другие, например JavaScript, реализуют возможности и ООП, и ФП. Есть же и такие языки, где функциональное программирование невозможно в принципе.
Функциональные языки:
- Haskell: это наиболее популярный язык среди функциональных программистов. В нем реализована защита памяти, отличный сбор мусора, а также повышенная скорость, обусловленная ранней компиляцией машинного кода. Его богатая статическая система типов дает вам доступ к уникальным алгебраическим и полиморфным типам, которые делают процесс программирования более эффективным, а код более читаемым.
- Erlang: этот язык, как и его потомок, Elixir, заняли нишу лучших функциональных языков для параллельных систем. Несмотря на то, что в популярности он уступает Haskell, его нередко используют для бэкенд-программирования. В последнее время Erlang начал завоевывать внимание в сфере масштабируемых мессенджеров, таких как WhatsApp и Discord.
- Clojure: это ориентированный на функциональную парадигму диалект Lisp, который работает на виртуальной машине Java (JVM). Будучи преимущественно функциональным языком, он поддерживает как изменяемые, так и неизменяемые структуры данных, но при этом все же менее строг в функциональном плане, чем другие. Если вам нравится Lisp, то вы также полюбите и Clojure.
- F#: этот язык аналогичен Haskell (они находятся в одной языковой группе), но имеет меньше расширенных возможностей. Кроме того, в нем реализована слабая поддержка объектно-ориентированных конструкций.
Языки с функциональными возможностями
- Scala: этот язык поддерживает как ООП, так и ФП. Его наиболее интересная особенность в наличии строгой системы статической типизации, как в Haskell, которая помогает создавать строгие функциональные программы. При проектировании Scala среди прочих стояла задача решить многие критические проблемы Java, поэтому данный язык очень подходит для Java-разработчиков, желающих попробовать функциональное программирование.
- JavaScript: несмотря на то, что приоритет в этом языке не на стороне функциональной парадигмы, JavaScript уделяет ей немало внимания в связи со своей асинхронной природой. В нем также поддерживаются такие важные функциональные возможности, как лямбда выражения и деструктуризация. Вместе эти атрибуты выделяют JS как ведущий язык для ФП.
- Python, PHP, C++: эти мультипарадигмальные языки тоже поддерживают функциональное программирование, но уже в меньшей степени, чем Scala и JavaScript.
- Java: этот язык относится к языкам общего назначения, но приоритет в нем отдается ООП, основанному на классах. Несмотря на то, что добавление лямбда выражений в некотором смысле помогает реализовывать более функциональный стиль, в конечном итоге Java остается языком ООП. Он позволяет заниматься функциональным программированием, но при этом в нем недостает ключевых элементов, которые бы оправдывали его освоение именно с этой целью.
Принципы функционального программирования
Переменные и функции
Ключевыми составляющими функциональной программы являются уже не объекты и методы, а переменные и функции. При этом следует избегать глобальных переменных, потому что изменяемые глобальные переменные усложняют понимание программы и ведут к появлению у функций побочных эффектов.
Чистые функции
Для чистых функций характерны два свойства:
- они не создают побочных эффектов;
- они всегда производят одинаковый вывод при получении одинакового ввода, что еще можно называть как ссылочную прозрачность.
Побочные эффекты же возникают, если функция изменяет состояние программы, переписывает вводную переменную или в общем вносит какие-либо изменения при генерации вывода. Отсутствие же побочных эффектов снижает риски появления ошибок по вине чистых функций.
Ссылочная прозрачность означает, что любой вывод функции должен допускать замену на ее значение, не изменяя при этом результата программы. Этот принцип гарантирует, что вы создаете такие функции, которые выполняют только одну операцию и достигают согласованного вывода.
Ссылочная прозрачность возможна только, если функция не влияет на состояние программы или в общем не старается выполнить более одной операции.
Неизменяемость и состояния
Неизменяемые данные или состояния не могут изменяться после их определения, что позволяет сохранять постоянство стабильной среды для вывода функций. Лучше всего программировать каждую функцию так, чтобы она выводила один и тот же результат независимо от состояния программы. Если же она зависит от состояния, то это состояние должно быть неизменяемым, чтобы вывод такой функции оставался постоянным.
Подходы функционального программирования обычно избегают применения функций с общим состоянием (когда несколько функций опираются на одно состояние) и функций с изменяющимся состоянием (которые зависят от изменяемых функций), потому что они уменьшают модульность программы. Если же вы не можете обойтись без функций с общим состоянием, сделайте это состояние неизменяемым.
Рекурсия
Одно из серьезных отличий объектно-ориентированного программирования от функционального в том, что программы последнего избегают таких конструкций, как инструкции if else или циклы, которые в разных случаях выполнения могут выдавать разные выводы.
Вместо циклов функциональные программы используют для всех задач по перебору рекурсию.
Функции первого класса
Функции в ФП рассматриваются как типы данных и могут использоваться как любое другое значение. Например, мы заполняем функциями массивы, передаем их в качестве параметров или сохраняем их в переменных.
Функции высшего порядка
Эти функции могут принимать другие функции в качестве параметров или возвращать функции в качестве вывода. Они делают возможности вызова функций более гибкими и позволяют легче абстрагироваться от действий.
Композиция функций
Для выполнения сложных операций функции можно выполнять последовательно. В этом случае результат каждой функции передается следующей функции в виде аргумента. Это позволяет с помощью всего одного вызова функции активировать целую серию их последовательных вызовов.
Функциональное программирование в Python
В Python реализована частичная поддержка ФП, и некоторые используемые в нем решения математических программ легче реализуются с помощью именно функционального подхода.
Самая сложная часть перехода к использованию такого подхода в сокращении числа используемых классов. В Python классы имеют изменяемые атрибуты, что усложняет создание чистых неизменяемых функций.
Попробуйте оформлять весь код на уровне модулей и переключайтесь на классы только по мере необходимости.
Давайте посмотрим, как добиться чистых неизменяемых функций и функций первого класса в Python, после чего познакомимся с синтаксисом для их композиции.
Чистые и неизменяемые функции
Многие из встроенных в Python структур данных являются неизменяемыми по умолчанию:
- integer;
- float;
- Boolean;
- string;
- Unicode;
- tuple.
Кортежи особенно полезны при использовании в качестве неизменяемой формы массива.
# код Python для проверки неизменяемости кортежейtuple1 = (0, 1, 2, 3)
tuple1[0] = 4
print(tuple1)
Этот код вызывает ошибку, потому что старается переопределить неизменяемый объект кортежа. Эти неизменяемые структуры данных рекомендуется использовать в функциональных программах Python для получения чистых функций.
Нижеприведенную функцию можно считать чистой, так как у нее нет побочных эффектов, и она всегда возвращает одинаковый вывод:
def add_1(x):
return x + 1
Функции первого класса
Отметим, что в Python функции рассматриваются как объекты, и ниже мы приводим краткое руководство по их возможному использованию:
Функции в качестве объектов
def shout(text):
return text.upper()
Передача функции в качестве параметра
def shout(text):
return text.upper()def greet(func):
# сохраняем функцию в переменной
greeting = func("Hi, I am created by a function passed as an argument.")
print greeting greet(shout)
Возвращение функции из другой функции
def create_adder(x):
def adder(y):
return x+y return adder
Композиция функций
Для компоновки функций в Python мы используем вызов lambda function . Это позволяет нам единовременно вызывать любое число аргументов.
import functoolsdef compose(*functions):
def compose2(f, g):
return lambda x: f(g(x))
return functools.reduce(compose2, functions, lambda x: x)
На строке 4 мы определяем функцию compose2 , получающую две функции в качестве аргументов f и g .
На строке 5 мы возвращаем новую функцию, представляющую композицию из f и g .
В завершении на строке 6 мы возвращаем результаты этой композиции функций.
Функциональное программирование в JavaScript
В связи с поддержкой функций первого класса JavaScript уже давно предлагает функциональные возможности. ФП на этом языке с недавних пор начало набирать популярность, так как повышает производительность при использовании в таких фреймворках, как Angular и React.
Давайте взглянем на то, как можно реализовывать разные функциональные принципы с помощью JS. Сосредоточимся мы на создании ключевых компонентов, а именно чистых функций, функций первого класса и композиций функций.
Чистые и неизменяемые функции
Чтобы начать создание чистых функций в JS, нам понадобится использовать функциональные альтернативы стандартному поведению, такие как const , concat и filter .
Стандартное ключевое слово let определяет изменяемую переменную. Если вместо него для объявления использовать const , это гарантирует нам неизменность переменной, так как переназначить ее уже не получится.
const heightRequirement = 46;function canRide (height) return height >= heightRequirement;
>
Функциональные альтернативы нам также нужно использовать для управления массивами. Стандартным способом добавления элемента в массив является метод push() . К сожалению, этот метод изменяет начальный массив, в связи с чем не считается чистым.
Но у нас есть его функциональный эквивалент — concat() . Вот он уже возвращает новый массив, который содержит все начальные элементы вместе с добавленным. В этом случае сам начальный массив остается неизменным.
const a = [1, 2]
const b = [1, 2].concat(3)
Для удаления элемента из массива мы обычно используем методы pop() и slice() . Тем не менее они не относятся к функциональным, так как изменяют именно первичный массив. Вместо них мы берем метод filter() , который создает новый массив со всеми элементами, прошедшими проверку условия.
const words = ['spray', 'limit', 'elite', 'exuberant', 'destruction', 'present'];const result = words.filter(word => word.length > 6);
Функции первого класса
JavaScript поддерживает функции первого класса по умолчанию. Вот краткое руководство по возможным действиям с функциями в этом языке:
Присвоение функции к переменной
const f = (m) => console.log(m)
f('Test')
Добавление функции в массив
const a = [
m => console.log(m)
]
a[0]('Test')
Передача функции в качестве аргумента
const f = (m) => () => console.log(m)
const f2 = (f3) => f3()
f2(f('Test'))
Возвращение функции из другой функции
const createF = () => return (m) => console.log(m)
>
const f = createF()
f('Test')
Функциональная композиция
В JavaScript мы можем компоновать функции при помощи цепочек вызовов:
obj.doSomething()
.doSomethingElse()
В качестве альтернативы можно передать выполнение функции в следующую функцию:
obj.doSomething(doThis())
Если же требуется связать больше функций, можно вместо этого использовать библиотеку lodash , которая позволит упростить их композицию. Если точнее, то мы передаем в качестве аргумента ее метод compose , сопровождаемый списком функций.
Первая функция в этом списке использует в качестве ввода начальный аргумент, а последующие функции наследуют свои вводные аргументы из вывода предшествующих.
import < compose >from 'lodash/fp'const slugify = compose(
encodeURIComponent,
join('-'),
map(toLowerCase),
split(' ')
)slufigy('Hello World') // hello-world
Функциональное программирование в Java
Java очень ограниченно поддерживает ФП по сравнению с Python или JS. Тем не менее в нем есть возможность имитировать функциональное поведение при помощи лямбда функций, потоков и анонимных классов.
В конце концов, компилятор Java создавался без учета функционального программирования, в связи с чем не может использовать многие из преимуществ этой парадигмы.
Чистые и неизменяемые функции
В Java есть несколько неизменяемых структур данных:
Вы также можете создавать собственные неизменяемые классы при помощи ключевого слова final .
// неизменяемый класс
public final class Student
<
final String name;
final int regNo; public Student(String name, int regNo)
<
this.name = name;
this.regNo = regNo;
>
public String getName()
<
return name;
>
public int getRegNo()
<
return regNo;
>
>
Ключевое слово final в классе предотвращает создание дочернего класса. Использование final для name и regNo делает невозможным изменение значений после построения объекта.
В этом классе также присутствуют параметризованный конструктор и геттеры для всех переменных, но при этом отсутствуют сеттеры, что помогает добиться для данного класса неизменяемости.
Функции первого класса
В Java для получения функций первого класса можно использовать лямбда функции. Лямбда принимает список выражений, например методов, но не требует имени или предварительного определения.
Лямбда выражения можно использовать вместо функций, так как они рассматриваются как стандартные объекты класса, которые можно передавать или возвращать.
// ФУНКЦИЯ ПЕРВОГО КЛАССА
Supplier lambda = myObject::toString;
// ФУНКЦИЯ ВЫСШЕГО ПОРЯДКА
Supplier higherOrder(Supplier fn) String result = fn.get();
return () -> result;
>
Композиция функций
Java содержит интерфейс, java.util.function.Function , предоставляющий методы для композиции функций. Метод compose сначала выполняет переданную ему функцию ( multiplyByTen ), а затем передает возвращаемое значение внешней функции ( square ).
И наоборот — метод andThen выполняет сначала внешнюю функцию, а затем функцию из своих параметров.
Function square = (input) -> input * input;
Function multiplyByTen = (input) -> input * 10;// COMPOSE: аргумент будет выполнен в начале
Function multiplyByTenAndSquare = square.compose(multiplyByTen);// ANDTHEN: аргумент будет выполнен в конце
Function squareAndMultiplyByTen = square.andThen(multiplyByTen);
На строках 1 и 2 мы сначала создаем две функции, square и multiplyByTen .
Затем на строках 5 и 8 мы делаем из них две композиции, multiplyByTenAndSquare и squareAndMultiplyByTen , каждая из которых принимает два аргумента (удовлетворяя условие square ).
Каждая из этих композиций выполняет обе изначальные функции, но в разном порядке. Теперь вы можете вызвать композиции для выполнения обеих исходных функций с одинаковым вводом.
Что изучать дальше
Сегодня мы пробежались по наиболее общим принципам функционального программирования и узнали, как они проявляются в Python, JavaScript и Java.
Одним из ведущих функциональных языков, переживающим этап возрождения, является Scala. Многие технологические гиганты, такие как Twitter и Facebook, начали использовать этот язык и уже ищут программистов с соответствующими навыками, поэтому рекомендуем выбрать в качестве следующего этапа на пути освоения ФП именно Scala.
Успехов вам в обучении!
- 5 ключевых понятий Python и их магические методы
- Proxy — сокровище JavaScript
- Аннотации для параллелизма в Java: расцвечивание потоков