ChatGPT на русском языке, бесплатноНовости и обновления в Telegram
На sponsr есть решения ваших задач
Полезные видео о фронтенде. Подпишись на Rutube
Что такое каррирование? Функциональное программирование
2 года назад·5 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Каррирование.
Это серия статей о функциональном программировании:
Представим, что у нас есть функция
Итак, сначала мы создали
Пояснение к функции
Функция
Каждый раз, когда мы вызываем
Каррирование действительно внесло улучшения, нам не нужно каждый раз указывать локаль. Вместо этого каррированная функция
Для лучшего понимания можно развернуть каррирование и вместо этого использовать обычные функции.
- Парадигмы программирования
- Композиция
- Функторы
- Каррирование (рассматривается в этой статье)
- Чистые функции
- Функции первого класса
Что такое каррирование?
Каррированная функция — это функция, которая продолжает возвращать функции до тех пор, пока не будут отправлены все ее параметры.Как работает каррирование?
Предположим, есть функция сложенияadd
.
Простейшая реализация каррирования — заставить функцию возвращать функцию и т. д.const add = (a, b) => a + b
Эту функцию можно использовать так.const add = (a) => (b) => a + b
const addOne = add(1) // addOne = (b) => 1 + b
curry
, которая принимает функцию и каррирует ее.
Как мы видим,const add = curry((a, b) => a + b)
curry
— это функция, которая использует другую функцию для ленивой обработки параметров. Итак, теперь мы можем вызвать ее следующим образом.
const addOne = add(1) // addOne = (b) => 1 + b
addOne
, передав 1
в качестве первого параметра (a
) каррированной функции добавления. Что привело к другой функции, которая ожидает остальные параметры, где логика добавления не будет выполняться, пока не будут предоставлены все параметры.
Теперь, передаваяaddOne(2) // 3
2
(как b
) в addOne
выполняет логику 1 + 2
.
Пояснение к функции curry
Функция curry
принимает функцию и делает ее параметры ленивыми, другими словами, вы предоставляете эти параметры по мере необходимости. Так же, как addOne
.
Вы по-прежнему можете вызывать каррированную версию функции добавления следующим образом.
Таким образом, он либо принимает аргументы по частям, либо все аргументы сразу.const three = add(1, 2)
Для чего нужно каррировать функции?
Каррирование делает код:- Чище
- Уменьшает количество повторяющихся параметров и делает код более лаконичным
- Более компонуемым
- Переиспользуемым
Почему каррирование делает код лучше?
Некоторые функции принимают конфигурацию в качестве входных данных
Если у нас есть функции, которые принимают конфигурацию (начальные настройки), нам лучше их каррировать, потому что эти конфигурации, вероятно, будут повторяться снова и снова. Предположим, что у нас есть функция перевода, которая принимает локаль и текст для перевода.Использование будет выглядеть так.const translate = (locale, text) => { /* логика перевода */ }
translate('ru', 'Hello') translate('ru', 'Goodbye') translate('ru', 'How are you?')
translate
, мы должны указывать язык и текст. Что является избыточным для предоставления локали при каждом вызове.
Но вместо этого давайте каррируем translate
следующим образом.
Теперьconst translate = curry( (locale, text) => { /* логика перевода */ } ) const translateToRu = translate('ru')
translateToRu
имеет ru
в качестве locale
, предоставленного каррированной функции translate
, и ожидает текста. Мы можем использовать это так.
translateToRu('Hello') translateToRu('Goodbye') translateToRu('How are you?')
translateToRu
содержит locale
из-за каррирования.
После каррирования - в этом конкретном примере код стал:
- чище
- менее многословным и менее избыточным.
На практике
На практике у нас есть динамическая локаль (у каждого пользователя свой язык), может бытьfr
, en
, de
или что-то еще. Поэтому вместо этого лучше переименовать translateToRu
в translateTo
, где translateTo
может быть загружен с любой локалью.
Теперь у нас есть translate
, который принимает locale
как конфигурацию и text
как данные. Благодаря тому, что translate
каррирован, мы смогли отделить параметры конфигурации от данных.
Зачем отделять конфигурации от данных?
Многие компоненты и функции нуждаются в использовании некоторой функциональности (в нашем случае,translateTo
), но не должны или не могут знать о части конфигурации (locale
). Эти компоненты или функции имеют только часть данных (text
). Таким образом, эти функции смогут использовать эту функцию без необходимости знать о части конфигурации.
Таким образом, этот компонент или функция будут меньше связаны с системой, что сделает компоненты более компонуемыми и более удобными в сопровождении.
Когда применять каррирование?
Когда мы знаем, что в функции есть конфигурация и есть данные, лучше их каррировать. Каррирование даст нам возможность их разделить. И это признак зрелого дизайна системы. Потому что одним из основных столпов качества кода является разделение задач. Даже если функции нужны все параметры для правильной работы, мы все равно лучше знаем, когда передавать параметры и на каком уровне приложения.Связь между замыканием и каррированием
Замыкание - это функция, возвращаемая «родительской» функцией и имеющая доступ к внутреннему состоянию родительской функции. Каррирование всегда приводит к замыканию. Потому что каждая функция, возвращаемая каррированной функцией, будет снабжена внутренним состоянием родителей.Примеры каррирования
Перед тем как продолжить
Добавим некоторые утилиты, чтобы мы могли перейти к примерам. Прототип массива имеет такие утилиты, какfilter
, map
и другие. Но они не поддерживают каррирование, потому что используют запись через точку (.
).
Итак, давайте конвертируем их в каррируемый формат.
Теперь мы можем использовать их так.const filter = (fn, list) => list.filter(fn) const map = (fn, list) => list.map(fn) const startsWith = (starter, s) => s.startsWith(starter)
Мы исключили запись через точку и передали обработанные данные в качестве последнего параметра. Затем мы их каррируем. Функцияconst lessThan21 = user => user.age < 21 // Вместо такого использования... const filteredUsers = users.filter(lessThan21 ) // ...будем использовать такое const filteredUsers = filter(lessThan21, users)
curry
будет принимать функцию и возвращать каррированную функцию.
const filter = curry((fn, list) => list.filter(fn)) const map = curry((fn, list) => list.map(fn)) const startsWith = curry((starter, s) => s.startsWith(starter))
Пример 1
Дан список чисел, нужно увеличить все числа на 1. Вход:[1, 2, 3, 4, 5]
Выход: [2, 3, 4, 5, 6]
Реализация:
// каррированная функция add была определена ранее const addOne = add(1) const incrementNumbers = map(addOne) const incrementedNumbers = incrementNumbers(numbers)
Пример 2
Дана строка, оставить все слова, начинающиеся с буквыc
.
Вход: "currying javascript function”
Выход: “currying”
Реализация:
const startsWithC = startsWith('c') const filterStartsWithC = filter(startsWithC) const filteredWords = filterStartsWithC(words)
Пример 3
Дан список диапазонов и список чисел. Создайте массив функций, которые могут фильтровать числа на основе предоставленных диапазонов.Выход: массив функций. Каждая функция может принимать числа и возвращать отфильтрованные числа, которые находятся в заданном диапазоне.const ranges = [ { min: 10, max: 100 }, { min: 100, max: 500 }, { min: 500, max: 999 } ] const numbers = [30, 50, 110, 200, 650, 700, 1000] // 30 и 50 в первом диапазоне // 110 и 200 во втором диапазоне // 650 и 700 в третьем диапазоне // 1000 не принадлежит ни одному диапазону
В этом примере есть двойное каррирование,const isInRange = curry( (range, val) => val > range.min && val < range.max ) const filters = ranges.map((range) => filter(isInRange(range)))
filter
и isInRange
.
filters
теперь представляют собой список функций, каждая из которых ожидает numbers
для обработки.
const isInRange = (range, val) => val > range.min && val < range.max const filters = ranges.map( (range) => (numbers) => numbers.filter( number => isInRange(range, number) ) )
Итоги
Каррирование просто делает параметры ленивыми. Когда функция продолжает возвращать функцию до тех пор, пока все ее аргументы не будут выполнены, она вычисляет и возвращает результат. Мы также увидели, как это делает наш код чище, более оаконичным, более компонуемым и даже более пригодным для повторного использования на практических примерах. И это тоже является примером принципа разделения ответственности.Функции первого класса. Функциональное программирование
2 года назад·4 мин. на чтение
В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функции первого класса.
Это серия статей о функциональном программировании:
Итак, вы спросите себя: «Хорошо, я понимаю взаимосвязь между функциональным программированием и математикой, но как первоклассные функции сделают возможными все эти преимущества?»
Очень хороший вопрос. Так как функциональное программирование полностью зависит от наличия привилегий функций, функции первого класса — это краеугольный камень для всех концепций функционального программирования.
Наличие в языке программирования функций первого класса позволяет иметь удивительные шаблоны, которые рассмотрим далее.
Рассмотрим каждую строчку.
Строка 1:
- Парадигмы программирования
- Композиция
- Функторы
- Каррирование
- Чистые функции
- Функции первого класса (рассматривается в этой статье)
Что такое функция первого класса?
Считается, что язык программирования поддерживает функции первого класса, если он не имеет ограничений на то, как функции могут создаваться или использоваться. Говорят, что язык программирования имеет функции первого класса, когда функции в этом языке рассматриваются как любая другая переменная. В общем, языки программирования накладывают ограничения на способы манипулирования вычислительными элементами. Говорят, что элементы с наименьшими ограничениями имеют статус первого класса. Вот некоторые из "прав и привилегий" первоклассных элементов:- Может быть назначен обычным переменным
- Передаются в качестве аргументов функциям
- Возвращается как результат функций
- Входит в любые структуры данных
Особенности функций первого класса
1. Функция первого класса может быть назначена обычным переменным
const string = "Foo" const num = 2 const bool = false const greet = (name) => `Hello ${name}` // ...другие примитивные типы данных greet('John') // Hello John
2. Функция первого класса передается в качестве аргумента в другие функции
Есть функцияconst nums = [1, 2, 3, 4, 5] const addOne = (n) => n + 1 const addedOne = nums.map(addOne) // [2, 3, 4, 5, 6]
addOne
, которая обрабатывается как переменная и передается в функцию .map
. При этом функция addOne
действительно является функцией первого класса.
3. Функция первого класса возвращается как результат функции
Функцияconst makeCounter = () => { let count = 0 return () => ++count } const counter = makeCounter() counter() // 1 counter() // 2 counter() // 3 counter() // 4
makeCounter
вернула функцию, которую мы присвоили переменной счетчика. Где переменная counter
теперь содержит обычную функцию.
4. Функция первого класса входит в любые другие структуры данных
Мы можем хранить функции в массивах и, как вы уже догадались, мы также можем хранить их в объектах и так же перебирать их.const wakeUp = name => `${name}, wake up early!` const takeShower = name => `${name}, take shower!` const workout = name => `${name}, workout!` const shutUp = name => `${name}, shut up!` const morningRoutines = [ wakeUp, takeShower, workout, shutUp ] morningRoutines.forEach(routine => routine('John')) // John, wake up early! // John, take shower! // John, workout! // John, shut up!
Почему функции первого класса важны
Функциональное программирование находится под сильным влиянием математики. Функциональное программирование хотело бы, чтобы математика была включена в каждую строку кода. Хотя математика состоит только из функций и переменных, она все равно очень мощная и выразительная. Это то, что пытается сделать и функциональное программирование - решать каждую отдельную проблему с использованием функций и только функций. Когда вы в языке программирования можете обращаться с функцией так же просто, как с переменной, этот язык будет гораздо более гибким и откроет много возможностей для улучшений. Функциональное программирование сделает ваш код более предсказуемым, тестируемым, повторно используемым, настраиваемым, кэшируемым, поддерживаемым, компонуемым и читабельным.Паттерны на основе функций первого класса
1. Функции высшего порядка (Higher-order functions)
Функции считаются функциями высшего порядка, когда они принимают функции в качестве аргументов (например, большинство методов Array,.map
, .filter
, .reduce
, .every
) и/или возвращают функцию в качестве результата (точно так же, как makeCounter
).
2. Замыкания
Замыкание — это функция, возвращаемая «родительской» функцией, и имеющая доступ к внутреннему состоянию родительской функции. Как и в предыдущем примере сmakeCounter
.
Чтобы уточнить, приведем еще один пример.
/*1*/ const add = (x) => (y) => x + y /*2*/ /*3*/ const add5 = add(5) // add5 = (y) => 5 + y /*4*/ const add10 = add(10) // add10 = (y) => 10 + y /*5*/ /*6*/ add5(1) // 6 /*7*/ add10(1) // 11
add
— это функция, которая принимает первый параметр x
и возвращает анонимную функцию, которая принимает второй параметр y
и возвращает x + y
.
Строка 3: выполнение add(5)
вернет функцию со значением 5
внутри нее. Компилятор/оптимизатор поймет это именно так:
Строка 4: точно такая же, как и строка 3. Выполнениеconst add5 = (y) => 5 + y
add(10)
вернет функцию со значением 10
внутри нее. Компилятор/Оптимизатор поймет это именно так:
Строка 6 и строка 7: это обычные вызовы функций для ранее «динамически» созданных функцийconst add10 = (y) => 10 + y
add5
и add10
.
После понимания того, что делает каждая строка, разберемся в терминологии для add
, add5
и add10
:
add
— функция высшего порядка. Почему? Потому что она возвращает функцию.- Но
add5
иadd10
являются замыканиями. Почему? Потому что они имеют значения5
и10
соответственно, заключенные (связанные) в лексической области видимости их родителя и все еще доступные им. (Вот почему, когда мы вызываемadd5(1)
, он будет использовать уже переданное5
дляadd
).