Расширенное функциональное программирование: применение функторов

JavaScript функциональное программирование
Расширенное функциональное программирование: применение функторов

Источник изображения: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Данные также не будут переданы.

Вот, подумайте о ReactuseEffectи в Редукс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(Прикладной функтор) — лучшая операция, взгляните на схематическую диаграмму, чтобы описать поток операций прикладного функтора:

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)).

applicative vs functor

Резюме и план

Мы начали с концепции чистых функций и побочных эффектов.LazyBox(ленивая оценка), таким образом вводя «специальное значение» функции в поле и как применять эту «функцию в поле», а затем вводя связь между каррированием функций и применением функторов (по коробочным функциям должно быть каррировано ); затем используйте расширенныйEitherЧтобы выполнить проверку формы, разделите функции и, наконец, введите использование бесточечного стиля для написания цепочек вызовов.

строить планы

До сих пор все проблемы, которые мы обсуждали, были синхронными проблемами, но в мире Javascript 90% кода является асинхронным.Можно сказать, что асинхронность является основным направлением мира JavaScript.Кто может решить асинхронные проблемы более элегантно? большая звезда в JavaScript, отcallback,прибытьPromise, затем кasync await, то как решить асинхронность в функциональном программировании, мы представим тяжеловесную концепцию в следующей главе.Monadтак же как异步函数的组合.

Ссылки и цитируемые статьи:

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы набираем front-end, iOS и Android круглый год.Если вы готовы сменить работу и любите облачную музыку, присоединяйтесь к нам на grp.music-fe(at)corp.netease.com!