Источник изображения:UN splash.com/photos/FQ YM…
Автор этой статьи:Чжао Сянтао
предыдущая глававведен вFunctor(函子)
Проще говоря, концепция заключается в том, чтобы заполнить «значение» в «коробке», а затем использоватьmap
Карта методов преобразует значения в Box:Box(1).map(x => x+1)
. В этой главе мы продолжаемBox
продолжать расширяться на основе других более мощных концепций, начиная счистая функцияа такжепобочный эффектПонятие и использованиеFunctor
Понятие и что будет введено дальшеApplicative Functor
введение.
Чистая функция — чрезвычайно важная концепция в функциональном программировании, и можно даже сказать, что она лежит в основе композиции функций. Возможно, вы слышали подобные утверждения: «Чистые функции — это ссылочная прозрачность», «Чистые функции не имеют побочных эффектов», «Чистые функции не имеют общего состояния». Ниже приводится краткое введение в чистые функции.
Чистые функции и побочные эффекты
В компьютерном программировании функция может быть описана как «чистая функция», если она удовлетворяет ограничениям следующих двух условий.
- При одинаковых параметрах возвращаемое значение функции должно быть одинаковым. Результирующее значение этой функции не зависит ни от какой-либо скрытой информации или состояния, которые могут быть изменены обработкой выполнения программы, ни от какого-либо внешнего ввода от ввода/вывода.
- Нет никаких семантически наблюдаемых побочных эффектов или выходных данных, таких как изменения объектов или операций, которые выводят на ввод-вывод, во время оценки возвращаемого функцией значения.
Первое правило чистых функций очень простое. Одни и те же входные данные всегда будут возвращать одни и те же выходные данные, что полностью похоже на «функцию», изучаемую в средней школе по математике. В нее передаются одни и те же параметры, и возвращаемое значение должно быть одинаковым. , "Карта" коллекций.
Что означает второе правило, когда нет наблюдаемых побочных эффектов? То есть функции не могут взаимодействовать с другими частями системы. Например: печать журналов, чтение и запись файлов, запросы данных, хранение данных и т. д.;
С точки зрения разработчика кода, если после запуска программы нет видимого эффекта, запускается ли она? Или цель кода достигается после запуска? Скорее всего, он просто тратит несколько циклов ЦП перед тем, как заснуть!
Необходимость иметь возможность взаимодействовать с постоянно меняющимся, общим и сохраняющим состояние DOM была неизбежна с момента появления языка JavaScript; что хорошего в базе данных, если вы не можете вводить или выводить какие-либо данные? Как могут отображаться наши страницы, если информация не может быть запрошена из сети? Без «побочного эффекта» мы вряд ли сможем двигаться,побочные эффекты неизбежны, любая из вышеперечисленных операций будет иметь побочные эффекты, нарушающие ссылочную прозрачность, и мы, похоже, стоим перед дилеммой!
Мир безопасен и полон закона, живущего согласно Татхагате.
какkeep pure
при условии правильного обращенияside effect
Шерстяная ткань?
Ленивый ящик - Ленивый ящик
Чтобы решить эту проблему идеально, мы обращаем внимание на основные функции JavaScript. Мы знаем, что в JavaScript функциями являются «гражданами первоклассных». JavaScript позволяет разработчикам манипулировать функциями, такими как переменные. Назначение значений в переменные, прохождение Функции как аргументы для других функций, функционируют как возвращаемое значение другой функции и т. Д.
Функции JavaScript имеютценностное поведение, то есть функция представляет собой неизменяемое значение, основанное на входных данных, которые еще не были оценены, или ее можно рассматривать как ленивое значение, ожидающее оценки. Затем мы можем поместить это «ленивое значение» вBox
, а затем задержите вызов, как в предыдущей главеBox
, может достичьLazy Box
:
const LazyBox = g => ({
map: f => LazyBox(() => f(g())),
fold: f => f(g())
})
Обратите внимание на наблюдение,Все, что делает функция карты, это объединение функций, функция на самом деле не вызывается; и вызов функции fold фактически выполнит вызов функции., см. пример:
const finalPrice = str =>
LazyBox(() => str)
.map(x => { console.log('str:', str); return x })
.map(x => x * 2)
.map(x => x * 0.8)
.map(x => x - 50)
const res = finalPrice(100)
console.log(res) // => { map: [Function: map], fold: [Function: fold] }
вызовfinalPrice
функция, он не распечатывает'str:100'
, указывая на то, что, как мы и ожидали, функция на самом деле не вызывается, а просто непрерывно составляет функции. без звонкаfold
До функции наш код был «чистым».
Это чем-то похоже на рекурсию, пока не будет выполнено условие завершения (без вызова
fold
ранее), рекурсивные вызовы будут продолжать складываться (объединенные функции) в стеке до тех пор, пока не будет выполнено условие завершения (вызовfold
функция), прежде чем начнется вычисление реальной функции.
const app = finalPrice(100)
const res2 = app.fold(x => x)
console.log(res2) // => 110
fold
Функции подобны рукам, открывающим ящик Пандоры.LazyBox
Мы бросили код, который мог бы «запачкать руки (с побочными эффектами)» в концеfold
, какой смысл это делать?
- Удалите нечистую часть кода, чтобы обеспечить «чистую» функцию основной части кода. Например, в приведенном выше коде только
app.fold(x => x)
"не чистый", остальные части "чистые" - Подобно централизованному управлению ошибками в предыдущей главе, можно
LazyBox
Для централизованного управления побочными эффектами, если мы продолжим расширять «чистую» часть проекта, мы можем даже выдвинуть нечистый код на край кода, чтобы обеспечить «чистую» и «ссылочную прозрачность» основной части.
LazyBox также работает с Rxjs в
Observable
Сходства много, оба ленивые, вsubscribe
До,Observable
Данные также не будут переданы.
Вот, подумайте о React
useEffect
и в Редуксreducer
,action
Изолированная концепция дизайна.
прикладной функтор
Function in Box
В предыдущем резюме я представил функцию загрузкиLazyBox
, и поместите его в конец, чтобы отложить выполнение, чтобы обеспечить «чистую» функцию большей части кода в конце.
Измените мышление, функцию можно рассматривать как «ленивое значение», тогда мы помещаем это немного особенное значение в обычноеBox
, что случится? Начнем с начальной школьной математики.
const Box = x => ({
map: f => Box(f(x)),
inspect: () => `Box(${x})`
})
const addOne = x => x + 1
Box(addOne) // => Box(x => x + 1)
inspect
Цель метода заключается в использованииconsole.log
Вызовите его неявно, чтобы мы могли просмотреть тип данных; этот метод невозможен в браузере, вы можете использоватьconsole.log(String(x))
вместо этого изменился API Node.js V12, вы можете использоватьSymbol.for('nodejs.util.inspect.custom')
заменятьinspect
Теперь у нас есть функция, которая оборачиваетBox
, но как мы используем эту функцию? после всегоBox(x).map
Все методы получают функцию! вернуться к функцииaddOne
выше, нам нужно число, переданное вaddOne
,Верно! Другими словами, как мы передаем число, чтобы применить этоaddOne
Что касается функции, то ответ очень прост, продолжайте передавать обернутое значение, а затемmap
Эта функция (addOne
) Нет, все в порядке! Посмотрите на код:
const Box = x => ({
map: f => Box(f(x)),
apply: o => o.map(x),
flod: f => f(x),
inspect: () => `Box(${x})`
})
Box(addOne).apply(Box(2)) // => Box(3)
Взгляните на волшебный новый метод Box, первое значение, которое нужно обернуть, этофункция х, затем переходим к проходу другогоBox(2)
войти, или вы можете использовать егоBox(2)
Вверхmap
вызов методаaddOne
функция!
Теперь взгляните на нас во второй разBox(addOne)
,Box(1)
, то проблема фактически сводится к следующему: поставитьfunctor
применить к другомуfunctor
на, и этоApplicative Functor
(Прикладной функтор) — лучшая операция, взгляните на схематическую диаграмму, чтобы описать поток операций прикладного функтора:
Итак, исходя из приведенного выше пояснения и примеров, можно сделать вывод: сначала поставить значениеx
вставитьBox
,Потомmap
функцияf
и поставить функциюf
вставитьBox
,Потомapply
один был установленBox
изx
, полностью эквивалентно!
F(x).map(f) == F(f).ap(F(x))
Box(2).map(addOne) == Box(addOne).apply(Box(2)) // => Box(3)
согласно сТехнические характеристики, после метода применения мы будем сокращать его как
ap
!
Applicative functor (应用函子)
Кроме того, это единственное, что является более «правдивым» среди множества «загадочных» концепций функционального программирования, подумайте об этом.Functor(函子)
Прикладные функторы и каррирование функций
Прежде чем перейти к каррированию функций, давайте рассмотрим исключение Гаусса в математике средней школы: пусть функцияf(x,y) = x + y
,существуетy = 1
, функция может быть изменена какf(x) = x + 1
. Основная идея состоит в том, чтобы превратить двоичную функцию в унарную, Точно так же мы можем преобразовать тернарную функцию в двоичную и даже преобразовать многомерную функцию в унарную.
Тогда мы можем думать, что процесс оценки функции в определенной степени является процессом исключения функции.Когда все элементы исключены, тогда можно вычислить значение функции.
Метод исключения Гаусса в математике чем-то похож на «каррирование» в функциональном программировании.Так называемое каррирование функций заключается в преобразовании функции, которая получает несколько параметров, в один параметр за раз до тех пор, пока не будут получены все параметры.После этого сделайте вызов функции (вычисление значения функции), см. пример:
const add = (x, y) => x + y
const curriedAdd = x => y => x + y
Что ж, после краткого понимания концепции каррирования функций, давайте продолжим и подумаем, если сейчас есть два «обернутых значения», как применить к нему функцию? Например:
const add = x => y => x + y
add(Box(1))(Box(2))
Приведенная выше схема, очевидно, не будет работать, мы не можем напрямую поставитьBox(1)
а такжеBox(2)
добавлено, что все они в коробке;
Но нам нужно неBox(1)
,Box(2)
,add
Примените три друг к другу, чтобы получить окончательный результатBox(3)
.
Начиная с первой главы, все наши функциональные операции выполняются под «защитой» Box, и теперь мы могли бы такжеadd
Функция обернута в Box, и вы получите функтор приложения.Box(add)
, а затем перейти к «применению» других функторов?
Box(add).ap(Box(1)) // => Box(y => 1 + y) (得到另一个应用函子)
Box(add).ap(Box(1)).ap(Box(2)) // => Box(3) (得到最终的结果)
Приведенный выше пример, потому что каждый разapply
Одинfunctor
, что равносильно уменьшению функции один раз, можно сделать вывод,Каррированная функция с несколькими аргументами, которую мы можем применить несколько раз..
Каждый раз при применении возвращается функтор применения, который оборачивает новую функцию, другими словами: применение нескольких данных к нескольким функциям, что очень похоже на несколько циклов.
Случаи использования прикладных функторов
Проверка формы является обычным требованием в нашей повседневной разработке. Для конкретного примера, если у нас есть регистрационная форма пользователя, нам необходимо проверить два поля имени пользователя и пароля. Общие коды следующие:
const checkUserInfo = user => {
const { name, pw, phone } = user
const errInfo = []
if (/^[0-9].+$/.test(name)) {
errInfo.push('用户名不能以数字开头')
}
if (pw.length <= 6) {
errInfo.push('密码长度必须大于6位')
}
if (errInfo.length) {
return errInfo
}
return true
}
const userInfo = {
name: '1Melo',
pw: '123456'
}
const checkRes = checkUserInfo(userInfo)
console.log(checkRes) // => [ '用户名不能以数字开头', '密码长度必须大于6位' ]
С этим кодом проблем нет, но если мы продолжим добавлять поля, которые необходимо проверить (например, номер телефона, электронная почта),checkUserInfo
Функция, несомненно, будет становиться все больше и больше, и если мы захотим изменить правила проверки поля, весьcheckUserInfo
Функции могут быть затронуты, и нам нужно добавить больше работы по модульному тестированию.
Вспомните действие «либо (влево или вправо)», представленное в главе 1.Right
относится к нормальной ветви,Left
Относится к ветке, где возникает исключение. Они никогда не появятся одновременно. Теперь давайте поймем это немного по-другому:Right
Относится к ветке, прошедшей проверку,Left
Относится к ветке, не прошедшей проверку.
На данный момент мы продолжаем расширять другие свойства и методы, основанные на любом из первой главы, которые используются в качестве инструментов для проверки формы:
const Right = x => ({
x,
map: f => Right(f(x)),
ap: o => o.isLeft ? o : o.map(x),
fold: (f, g) => g(x),
isLeft: false,
isRight: true,
inspect: () => `Right(${x})`
})
const Left = x => ({
x,
map: f => Left(x),
ap: o => o.isLeft ? Left(x.concat(o.x)) : Left(x),
fold: (f, g) => f(x),
isLeft: true,
isRight: false,
inspect: () => `Left(${x})`
})
По сравнению с оригиналомEither
, недавно добавленныйx
свойства иap
метод, остальные свойства полностью аналогичны, поэтому объяснения даваться не будут; новыйx
Причина атрибута в том, что информация об ошибке проверки формы должна быть записана, что хорошо понятно, а вновь добавленнаяisLeft
,isRight
Атрибуты проще, используются для различенияLeft/Right
ветвь.
Давайте подробнее рассмотрим недавно добавленноеap
метод, см. сначалаRight
разветвленныйap: o => o.isLeft ? o : o.map(x)
,без сомненийap
метод получает другойfunctor
, если другойfunctor
даLeft
например, вам не нужноRight
Процесс возвращается напрямую, если онRight
, то обычныйapplicative functor
то же самое, даo
как предметmap
.
Left
на веткеap: o => o.Left ? Left(x.concat(o.x)) : Left(x)
,еслиLeft
Например, выполняется «суперпозиция» для накопления информации об ошибках, и если нетLeft
Экземпляр , напрямую возвращает записанное сообщение об ошибке.
После выполнения подготовительной работы мы можем разделить его по функциональному мышлению (сочетанию функций).checkUserInfo
функция:
const checkName = name => {
return /^[0-9].+$/.test(name) ? Left('用户名不能以数字开头') : Right(true)
}
const checkPW = pw => {
return pw.length <= 6 ? Left('密码长度必须大于6位') : Right(true)
}
Вышеупомянутые две проверки полей разделены на две функции из одной функции и, что более важно, полная развязка; возвращаемое значение либо не проходит проверкуLeft
, либо проверка прошлаRight
, так что мы можем понять, что теперь есть дваEither
, пока у нас есть другойФункция, обернутая в блок Does и дважды каррированнаяНельзя ли сделать так, чтобы они применялись друг к другу?
const R = require('ramda')
const success = () => true
function checkUserInfo(user) {
const { name, pw, phone } = user
// 2 是因为我们需要 `ap` 2 次。
const returnSuccess = R.curryN(2, success);
return Right(returnSuccess)
.ap(checkName(name))
.ap(checkPW(pw))
}
const checkRes = checkUserInfo({ name: '1Melo', pw: '123456' })
console.log(checkRes) // => Left(用户名不能以数字开头密码长度必须大于6位)
const checkRes2 = checkUserInfo({ name: 'Melo', pw: '1234567' })
console.log(checkRes2) // => Right(true)
СейчасcheckUserInfo
Возвращаемое значение функции — либо функтор (левый, либо правый), который можно использовать позже.fold
Функция, показывающая, что проверку не проходит всплывающее окно или отправляется следующая форма.
Об использовании параметров проверкиValidationБольше подходят функторы.Чтобы сосредоточиться на объяснении основной линии концепции аппликативного функтора, мы не будем продолжать вводить новые понятия.
Стиль PointFree
пример вышеcheckUserInfo
функция, потребностьap
Дважды это кажется немного громоздким (подумайте, что, если нам нужно проверить больше полей?), мы можем абстрагировать функцию стиля без точек, чтобы сделать вышеописанное:
const apply2 = (T, g, funtor1, functor2) => T(g).ap(funtor1).ap(functor2)
function checkUserInfo(user) {
const { name, pw, phone } = user
const returnSuccess = R.curryN(2, success);
return apply2(Right, returnSuccess, checkName(name), checkPW(pw))
}
apply2
У функции много параметров, особенно нужно передатьT
Этот неопределенный контейнер используется для хранения обычных функций.g
Упакуйте в коробку.
Поместите «значение» (любой допустимый тип, включая функции, конечно) в контейнер (Box или Context). Существует унифицированный метод, называемый
of
, и этот процесс называетсяlift
, что означает продвижение: то есть продвигать значение в контексте.
Оглянитесь на то, что было представлено ранее:Box(addOne).ap(Box(2))
а такжеBox(2).map(addOne)
по результату(Box(3)
) выглядит так же. То есть выполнить операцию сопоставления (map(addOne)
) эквивалентно выполнению (Box(addOne)
), затем выполните ap (ap(Box(2))
), что может быть выражено в виде формулы:
F(f).ap(F(x)) == F(x).map(f)
Применяя формулу, мы можем изменить упрощеннуюapply2
в теле функцииT(g).ap(funtor1)
дляfuntor1.map(g)
, см. сравнение ниже:
const apply2 = (T, g, funtor1, functor2) => T(g).ap(funtor1).ap(functor2)
const liftA2 = (g, funtor1, functor2) => funtor1.map(g).ap(functor2)
Видите ключевой момент выше? надliftA2
Функция больше не привязана к конкретному типу коробки «Т», который является более общим и гибким.
В соответствии с приведенной выше теорией его можно переписатьcheckUserInfo
Функция:
function checkUserInfo(user) {
const { name, pw, phone } = user
const returnSuccess = R.curryN(2, success);
return liftA2(returnSuccess, checkName(name), checkPW(pw))
}
Теперь предположим, что мы добавили третье поле «номер мобильного телефона», которое нужно проверить, тогда функцию liftA2 можно расширить до liftA3, liftA4 и т. д.:
const liftA3 = (g, funtor1, functor2, functor3) => funtor1.map(g).ap(functor2).ap(functor3)
const liftA4 = (g, funtor1, functor2, functor3, functor4) => funtor1.map(g).ap(functor2).ap(functor3).ap(functor4)
Сначала вы можете почувствовать
liftA2-3-4
Выглядит некрасиво и ненужно, смысл такого способа написания в том, что количество фиксированных параметров вообще предусмотрено в функциональной либе, поэтому не нужно писать эти коды вручную.
Различия и связи между аппликативным функтором и функтором
согласно сF(f).ap(F(x)) == F(x).map(f)
, можно сделать вывод, что если ящик (Box) реализуетap
метод, то мы должны быть в состоянии использоватьap
метод выводитmap
метод, если у вас естьmap
метод, то этоFunctor
, поэтому мы также можем рассмотретьApplicative
даFunctor
расширение, чемFunctor
Более могущественный.
Так где сила?Functor
может отображать только функцию, которая принимает один аргумент (например,x => y
), если мы хотим поместить функцию, которая принимает несколько аргументов (например,x => y => z
) применяется к нескольким значениям, затемApplicative
сцена, подумай об этомcheckUserInfo
пример.
Нет сомнения, что Applicative Funtor может
apply
Несколько раз (включая один, конечно), тогда, если функция имеет только один параметр, можно считать, чтоmap
а такжеapply
эквивалентны, другими словами:map
эквивалентноapply
однажды.
Выше приведено сравнение в практическом применении на абстрактном математическом уровне:
- Functor: применить функцию к обернутому значению:
Box(1).map(x => x+1)
. - Applicative: применить обернутую функцию к обернутому значению:
Box(x => x+1).ap(Box(1))
.
Резюме и план
Мы начали с концепции чистых функций и побочных эффектов.LazyBox
(ленивая оценка), таким образом вводя «специальное значение» функции в поле и как применять эту «функцию в поле», а затем вводя связь между каррированием функций и применением функторов (по коробочным функциям должно быть каррировано ); затем используйте расширенныйEither
Чтобы выполнить проверку формы, разделите функции и, наконец, введите использование бесточечного стиля для написания цепочек вызовов.
строить планы
До сих пор все проблемы, которые мы обсуждали, были синхронными проблемами, но в мире Javascript 90% кода является асинхронным.Можно сказать, что асинхронность является основным направлением мира JavaScript.Кто может решить асинхронные проблемы более элегантно? большая звезда в JavaScript, отcallback
,прибытьPromise
, затем кasync await
, то как решить асинхронность в функциональном программировании, мы представим тяжеловесную концепцию в следующей главе.Monad
так же как异步函数的组合
.
Ссылки и цитируемые статьи:
- Functor, Applicative, and Why
- Applicative and list
- Functors, Applicatives, And Monads In Pictures
- Applicative Functors and data validation
- validation: A data-type like Either but with an accumulating Applicative
- Understanding Functor and Monad With a Bag of Peanuts
- How to deal with dirty side effects in your pure functional javascript
- Функциональное программирование на JavaScript — с практическими примерами
- Функциональное программирование JavaScript
Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы набираем front-end, iOS и Android круглый год.Если вы готовы сменить работу и любите облачную музыку, присоединяйтесь к нам на grp.music-fe(at)corp.netease.com!