Парадигмы программирования - императивная и декларативная
2 года назад·5 мин. на чтение
В этой статье поговорим о парадигмах программирования. Затронем императивную и декларативную парадигмы. Для сравнения разберем несколько небольших примеров. В конце мы взглянем на парадигмы с точки зрения эволюции.
Это серия статей о функциональном программировании:
Как видим, если мы хотим сравнить обе парадигмы (императивную и декларативную), то декларативная парадигма (в нашем случае Функциональная парадигма) больше похожа на шестеренки. Вы разрабатываете свои шестеренки как отдельные единицы, затем добавляете их туда, где они вам нужны. Но в императивной парадигме это больше похоже на тесто. Почти все смешано и слито в один и тот же кусок код.
В целом декларативная парадигма — это:
- Парадигмы программирования (рассматривается в этой статье)
- Композиция
- Функторы
- Каррирование
- Чистые функции
- Функции первого класса
Парадигмы программирования
Парадигма программирования — это стиль или "способ" программирования. Поэтому некоторые языки заставляют нас писать в определенной парадигме. Другие языки оставляют варианты открытыми для программиста, где каждая парадигма следует набору понятий. За всю историю компьютерного программирования инженеры разработали разные языки. Каждый язык основывался на одной или нескольких парадигмах. Эти парадигмы принадлежат к одной из следующих двух основных категорий:1. Императивная парадигма
В императивных языках программирования поток управления является явным, где программист инструктирует программу, как изменить ее состояние. В императивную парадигму также включается:- Структурная парадигма
- Объектно-ориентированная парадигма
2. Декларативная парадигма
В декларативной парадигме поток управления является неявным, когда программист указывает программе, что следует делать, не указывая, как это должно быть сделано. В декларативную парадигму также включается:- Функциональная парадигма
- Логическая парадигма
- Математическая парадигма
- Реактивная парадигма
Императивная парадигма
Императивная парадигма немного изменилась из-за структурной парадигмы, но у нее все еще есть проблемы:- Указание программе, как что-то делать (поток управления является явным)
- Общее состояние
Проблема 1: Указание программе, как что-то делать (поток управления является явным)
Кейс: представьте себе 1000 сотрудников с руководителем, который ведет их по проекту. Руководитель начинает рассказывать 1000 сотрудников, как делать вещи одну за другой. Как вы думаете, насколько это будет плохо? Я почти уверен, что вы видите, что этот стиль управления на микроуровне имеет большие риски, ловушки и даже не сработает. Решение: Сгруппировать людей по зонам ответственности и делегировать в каждую группу руководителя группы. Руководитель каждой группы должен знать, как делать что-то для достижении цели. Это значительно уменьшит сложность, узкие места и станет намного проще в управлении. В этой аналогии- Руководитель = Программист
- Руководители групп = Функции более высокого уровня
- Сотрудники в каждой группе = Строки кода
Проблема 2: Общее состояние
Кейс: Представьте отца, у которого двое детей. У них есть общий банковский счет. Каждый месяц отец кладет на этот счет 1000 долларов. Оба ребенка не знают, что учетная запись используется совместно. Таким образом, они оба думают, что у каждого есть 1000 долларов, которые он может потратить на себя. В конце месяца оказывается, что на этом счету осталось -1000 долларов. Решение: У каждого ребенка должна быть отдельная учетная запись и указанная ежемесячная сумма. В этой аналогии:- Дети = Функции
- Общий банковский счет = общее состояние
Пример императивной парадигмы
Давайте посмотрим, как функция для суммирования может быть реализована в императивной парадигме.Почему этот код считается императивным?const sum = (list) => { let result = 0 for (let i = 0; i < list.length; i++) { result += list[i] } return result }
-
Указание программе, как что-то делать (поток управления является явным): мы явно сообщаем циклу
for
, как работать. Также мы обращаемся к каждому элементу в массиве явно. - Совместное состояние: результирующая переменная является общим состоянием, изменяющимся на каждой итерации (с общим состоянием в более крупных решениях будет гораздо сложнее справиться).
Декларативная парадигма
Декларативная парадигма — это когда программист указывает программе, что должно быть сделано, не указывая, как. В декларативной парадигме мы пишем функции, которые:- Описывают, что должна выполнять программа, а не как (неявный поток управления).
- Не производят побочных эффектов (о которых мы поговорим позже).
Пример декларативной парадигмы
Мы увидели, как функцияsum
может быть реализована в императивной парадигме. Давайте посмотрим, как ее можно реализовать декларативно.
Похоже на магию? Но почему это считается декларативным?const add = (a, b) => a + b const sum = (list) => list.reduce(add)
- Описано, что программа должна выполнять, а не как (неявный поток управления): нет явного итератора, нет явного указания циклу, как работать или как получить доступ к элементам. Это было достигнуто с помощью метода
reduce
. - Не производит побочных эффектов: общее состояние — это форма побочных эффектов, которая была полностью устранена с помощью метода
reduce
и функцииadd
.
Еще одно сравнение
Что, если мы хотим суммировать только четные числа? Разберем эту задачу на примерах в разных парадигмах.Императивная реализация
const evenSum = (list) => { let result = 0 for (let i = 0; i < list.length; i++){ if(list[i] % 2 === 0) { result += list[i] } } return result }
Декларативная реализация
const evenSum = (list) => { const isEven = (n) => n % 2 const add = (a, b) => a + b return list.filter(isEven).reduce(add) }
- Предсказуемость
- Тестируемость
- Многоразовость
- Настраиваемость
- Кэшируемость
- Поддерживаемость
- Компонуемость
sum
, но будут иметь смысл в следующих статьях о функциональном программировании.
Эволюция парадигм
Итак, у нас есть 2 основные парадигмы: императивная и декларативная, каждая из которых имеет подпарадигмы. Теперь поговорим подробнее о структурной, объектно-ориентированной и функциональной парадигмах. с эволюционной точки зрения. Каждая парадигма ограничивала способ программирования, вводя что-то новое.-
Структурная парадигма: ограниченное использование
goto
и «потока передачи управления» за счет введения в наш код такой структуры, какif
/else
/then
/loop
и других. Другими словами, он ограничивает поток передачи управления. - Объектно-ориентированная парадигма: ограничение полиморфизма с использованием указателей на функции за счет введения полиморфизма с использованием наследования.
- Функциональная парадигма: ограничения общего состояния и побочные эффекты за счет введения иммутабельности.
Итоги
В реальной жизни у нас разные парадигмы с разными стилями, которые требуют разного уровня мастерства. Практика большего количества парадигм даст вам больше возможностей. У объектно-ориентированной парадигмы есть свои особенности, у функционального программирования - свои. Чем сильнее вы становитесь в этих парадигмах, тем мощнее будут ваши решения.Композиция функций. Функциональное программирование
2 года назад·5 мин. на чтение
Композиция — это способ построения больших модулей из более мелких. В этой статье подробно рассмотрим композицию на примере JavaScript.
Это серия статей о функциональном программировании:
Пример работы функции
Рассмотрим пример с более конкретными функциями:
Функция
Реализация
Добавим
Добавим для всех параметров, кроме
- Парадигмы программирования
- Композиция (рассматривается в этой статье)
- Функторы
- Каррирование
- Чистые функции
- Функции первого класса
Что такое композиция функций?
Композиция — это процесс объединения небольших единиц в более крупные, которые решают более крупные задачи. При композиции входные данные одной функции приходят из выходных данных предыдущей.Как работает композиция?
Математическая запись определения композиции выглядит следующим образом:В JavaScript композиция, где(f ∘ g)(x) = f(g(x))
f
и g
- это функции, будет выглядеть так:
Функцияconst compose = (f, g) => (x) => f(g(x))
compose
является композицией функций f
и g
. Выходные данные функции g
будут переданы на вход функции f
. По другому эта запись выглядела бы следующим образом:
const compose = (f, g) => { return (x) => { const gResult = g(x) const fResult = f(gResult) return fResult } }
Пример работы функции compose
Рассмотрим пример с более конкретными функциями:
Теперь составим композицию из этих маленьких функций -const getAge = (user) => user.age const isAgeAllowed = (age) => age >= 30
getAge
и isAgeAllowed
:
const user = { name: 'John', age: 35 } const isAllowedUser = compose( isAgeAllowed, getAge ) isAllowedUser(user) // true
compose
выполняет функции справа налево. Мы отправляем объект user
в функцию isAgeAllowed
. Далее user
попадает сначала в getAge
, потом результат этой функции попадает в isAgeAllowed
.
compose
и pipe
Функция pipe
очень похожа на функцию compose
. Они выполняют одну и ту же роль. Обе объединяют функции в цепочки. Однако их реализация и порядок выполнения функций отличается.
Реализация compose
и pipe
compose
реализован следующим образом:
const compose = (...fns) => { return (x) => fns.reduceRight((acc, fn) => fn(acc), x) }
pipe
реализован следующим образом:
Они отличаются лишь в применении функцииconst pipe = (...fns) => { return (x) => fns.reduce((acc, fn) => fn(acc), x) }
reduce
и reduceRight
. Это влияет лишь на порядок выполнения функций.
Порядок выполнения функций
compose
выполняет функции справа налево.
pipe
выполняет функции слева направо.
Пример
Предположим имеется 3 функции:f
, g
и h
.
При использовании compose
:
Порядок выполнения функций будет такимcompose(f, g, h) ← ← ←
h
, g
и далее f
(справа налево). И выходные данные будут передаваться в следующую функцию.
При использовании pipe
:
Порядок выполнения функций будет такимpipe(f, g, h) → → →
f
, g
и далее h
(слева направо). И выходные данные будут передаваться в следующую функцию.
Какую функцию использовать?
compose
и pipe
не сильно отличаются друг от друга. Они решают одну и ту же задачу. compose
ближе к математической нотации (f ∘ g)(x) = f(g(x))
. С pipe
визуально легче воспринимать порядок выполнения функций.
Реальный пример
В следующих примерах будем использоватьpipe
. Предположим, нужно сделать простой калькулятор цен, в котором нужно применить:
- налог (30%, по умолчанию),
- сервисный сбор (10у.е., по умолчанию),
- скидку,
- купон,
- цену доставки на основе веса.
const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { // реализация }
Реализация без композиции
Эта функция выполняет свою задачу, но ее сложно читать, тестировать и отлаживать.const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { return ( price - (price * percentCoupon) - discount + (weight * shippingPricePerKg) + serviceFees ) * (1 + taxPercentage) }
Реализация с композицией
Эта реализация выглядит более чистой, ее легко тестировать и отлаживать. Все это благодаря модульности.const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { const applyTax = (val) => val * (1 + taxPercentage) const applyServiceFees = (val) => val + serviceFees const applyPercentCoupon = (val) => val - val * percentCoupon const applyDiscount = (val) => val - discount const applyShippingCost = (val) => val + weight * shippingPricePerKg return pipe( applyPercentCoupon, applyDiscount, applyShippingCost, applyServiceFees, applyTax )(price) }
Запуск примера
Запустим, передав толькоprice
. Очевидно, получим неверный результат, т.к. передали не все обязательные параметры.
Сначала продебажим. Для дебагаpriceCalculator(10) // NaN
pipe
и compose
можно добавить удобную функцию inspect
. Она просто логирует в консоль входные данные и возвращает эти данные без изменений.
const inspect = (label) => (x) => { console.log(`${label}: ${x}`) return x }
inspect
в цепочку выполнения.
Результат будет примерно таким:const priceCalculator = ( taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg ) => { const applyTax = (val) => val * (1 + taxPercentage) const applyServiceFees = (val) => val + serviceFees const applyPercentCoupon = (val) => val - val * percentCoupon const applyDiscount = (val) => val - discount const applyShippingCost = (val) => val + weight * shippingPricePerKg return pipe( inspect('price'), applyPercentCoupon, inspect('after applyPercentCoupon'), applyDiscount, inspect('after applyDiscount'), applyShippingCost, inspect('after applyShippingCost'), applyServiceFees, inspect('after applyServiceFees'), applyTax )(price) }
Видим,priceCalculator(10) // price: undefined // after applyPercentCoupon: NaN // ...
price
- undefined
. Это произошло потому что price
- третий аргумент, а мы передаем его первым.
Быстрый фикс
Сделаем так, чтобы функция принимала один объект:Далее, используем:const priceCalculator = ({ taxPercentage = 0.3, serviceFees = 10, price, discount, percentCoupon, weight, shippingPricePerKg }) => { // ... }
Все еще получаемpriceCalculator({ price: 10 }) // price: 10 // after applyPercentCoupon: NaN // ...
NaN
, в этот раз потому что не был передан percentCoupon
и он имеет значение undefined
.
price
, значения по умолчанию.
Если запустить заново, получим результат:const priceCalculator = ({ taxPercentage = 0.3, serviceFees = 10, price, discount = 0, percentCoupon = 0, weight = 0, shippingPricePerKg = 0 }) => { // ... }
Это пример того как композиция позволяет нам тестировать и исправлять код проще и быстрее, просто проверяя области, которые вызывают подозрения. Проблема, которую мы отлаживали, была очень простой. Когда мы переходим к более масштабным модулям, все становится еще более трудным для проверки. Большие атомарные функции трудно поддерживать. Разделение функций на более мелкие упрощает отладку, тестирование, поддержку и разработку функций.priceCalculator({ price: 10 }) // 26
Для чего нужна композиция?
Композиция это объединение меньших модулей в более крупные. Если мы думаем оперируем модулями (что обеспечивается композицией), мы улучшаем:- модульное мышление,
- тестируемость,
- возможность отладки,
- поддерживаемость.