Автор этой статьи:Чжао Сянтао
Концепция функционального программирования (Functional Programming) постепенно становится популярной как во front-end, так и в back-end областях.Сейчас широкомасштабные приложения, не использующие технологии функционального программирования, встречаются редко, как, например, популярный front-end React (основная идея ) Data is a view), Composition API Vue3.0, Redux, Lodash и другие интерфейсные фреймворки и библиотеки полны функционального мышления.На самом деле функциональное программирование ни в коем случае не является парадигмой программирования, созданной в последние годы. начало информатики,Alonzo Churchопубликовано в 1930-х годахlambdaМожно сказать, что исчисление — это прошлое и настоящее функционального программирования.
Эта серия статей подходит для людей, у которых есть прочная основа JavaScript и некоторый опыт функционального программирования.Цель этой статьи — объединить языковые особенности JavaScript для объяснения некоторых концепций теории категорий и практического применения логики в программировании.
Проклятие Черной Жемчужины
начать плавание!
Сначала давайте посмотрим на双11大促销Код для , и как обзор таких концепций, как композиция функций, и как первый шаг в новом путешествии, которое вот-вот начнется:
const finalPrice = number => {
const doublePrice = number * 2
const discount = doublePrice * .8
const price = discount - 50
return price
}
const result = finalPrice(100)
console.log(result) // => 110
Взгляните на приведенный выше простой双11购物狂欢节код,Первоначальная цена 100, после причудливой большой акции со стороны торговца (打折(八折) + 优惠券(50)), все успешно получилиЦена рубки 110.Хорошая сделка, поторопитесь
Если вы читали другую статью нашей команды по работе с облачной музыкойВведение в статьи по функциональному программированию, я уверен, что вы уже знаете, как писать функциональные программы: программы, которые передают данные между рядом чистых функций через конвейеры. Мы также знаем, что эти программы являются декларативными кодексами поведения. Теперь снова используйте идею композиции функций, чтобы сохранить операции конвейера данных и исключить так много промежуточных переменных, сохраняя своего родаPoint-Freeстиль:
const compose = (...fns) => x => fns.reduceRight((v, f) => f(v), x)
const double = x => x * 2
const discount = x => x * 0.8
const coupon = x => x - 50
const finalPrice = compose(coupon, discount, double)
const result = finalPrice(100)
console.log(result) // => 110
Эм! Наконец-то появился функциональный вкус! В это время мы обнаружили, что функция переданаfinalPriceпараметры100Подобно заводскому компоненту, он последовательно функционирует на конвейере.double,discountа такжеcouponэксплуатировался.100Циркулирует в трубах, как вода. Увидев эту сцену, мы немного знакомы, массивmap,filter, разве это не полностью аналогичная концепция? Итак, мы можем обернуть наши входные параметры в массив:
const finalPrice = number =>
[number]
.map(x => x * 2)
.map(x => x * 0.8)
.map(x => x - 50)
const result = finalPrice(100)
console.log(result) // => [110]
Теперь мы ставимnumberПоместите его в контейнер Array, а затем вызовите map три раза подряд, чтобы реализовать конвейерный поток данных. Тщательное наблюдение показало, что Array — это просто контейнер для наших данных, мы просто хотим использовать метод отображения массива, мы не можем использовать другие методы в настоящее время, так почему бы не создатьBoxЧто с контейнером?
const Box = x => ({
map: f => Box(f(x)),
inspect: () => `Box(${x})`
})
const finalPrice = str =>
Box(str)
.map(x => x * 2)
.map(x => x * 0.8)
.map(x => x - 50)
const result = finalPrice(100)
console.log(result) // => Box(110)
Причина, по которой функция Box используется вместо класса ES6 для создания объектов, заключается в том, чтобы избежать «плохих» ключевых слов new и this (из «You Don’t Know JS Volume 1»), new заставляет людей ошибочно думать, что класс создан , но нет такой вещи, как
实例化, просто属性委托机制(对象组合的一种), и это вводит контекст выполнения и проблемы с лексической областью видимости, и я просто пытаюсь создать простой объект!Цель метода проверки — использовать console.log в Node.js для его неявного вызова, чтобы мы могли просмотреть тип данных; этот метод невозможен в браузере, вы можете использовать
console.log(String(x))вместо этого изменился API Node.js V12, вы можете использоватьSymbol.for('nodejs.util.inspect.custom')Альтернатива осмотруПричина использования цепочки последовательных точек, точек, точек вместо составления состоит в том, чтобы упростить понимание, составляющий более функционален.
Запечатанная Черная Жемчужина
это в коробкеmapсо знаменитым массивомmapто же, за исключением того, что первый работаетBox(x)в то время как последний[x]. Они также используются почти таким же образом, бросая значение в Box, а затем постоянно карта, карта, карта...:
Box(2).map(x => x + 2).map(x => x * 3);
// => Box(12)
Box('hello').map(s => s.toUpperCase());
// => Box('HELLO')
Это первый контейнер для функционального программирования, назовем егоBox, а данные как Черная жемчужина в бутылке Капитана Джека, мы можем только передатьmapметод управления значением в нем, а Box подобен виртуальному барьеру, который также можно сказать защищает значение в Box от произвольного приобретения и манипулирования в определенной степени.
Зачем использовать этот ход мыслей? Потому что мы можем манипулировать значениями внутри контейнера, не покидая Box. Значение в поле передаетсяmapПосле функции мы можем делать все, что захотим; после завершения операции, чтобы предотвратить несчастные случаи, верните ее на место.Box. В результате этого мы можем непрерывно вызыватьmap, запустить любую функцию, которую мы хотим. Можно даже изменить тип значения, как в последнем примере выше.
map — это эффективный и безопасный способ преобразования значений внутри контейнера с помощью лямбда-выражений.
подождите, если бы мы могли продолжать звонитьmap.map.map, то можем ли мы назвать этот тип какMappable TypeНет абсолютно никаких проблем с этим пониманием!
mapУметь отображать значения функций в контексте. Сначала он открывает контейнер, затем сопоставляет значение с другим значением через функцию и, наконец, помещает полученное значение в новый контейнер того же типа. И этот метод преобразования значений в контейнере (карте) называетсяФунктор.
Functor(函子)даТеория категорийконцепция. Что такое теория категорий? ? ? Я не понимаю! ! !
Не паникуйте! Позже мы продолжим кратко обсуждать понятие и теорию теории категорий и Функтора, давайте на время забудем странное название и пока пропустим это понятие.
Давайте продолжим и назовем это Коробкой, которую мы все можем понять!
Искупление черной жемчужины
похожий наBox(2).map(x => x + 2)Мы уже можем обернуть значение любого типа в Box, а затем продолжать сопоставлять, сопоставлять, сопоставлять... .
Другой вопрос, как мы получаем нашу ценность? Результат, который я хочу, это4вместоBox(4)!
Что хорошего в Черной жемчужине, если ее нельзя выпустить из бутылки? Затем пусть капитан Джек Воробей схватит меч Черной Бороды и выпустит Черную Жемчужину!
Пришло время добавить еще один метод к нашему самому примитивному Box.
const Box = x => ({
map: f => Box(f(x)),
fold: f => f(x),
inspect: () => `Box(${x})`
})
Box(2)
.map(x => x + 2)
.fold(x => x) // => 4
Ах, посмотриfoldа такжеmapразница?
mapзаключается в том, чтобы переупаковать результат выполнения функции в Box и вернуть новый тип Box, аfoldЭто вернуть результат выполнения функции напрямую, и все!
Практическое применение коробки
Try-Catch
Ошибки JavaScript могут возникать во многих ситуациях, особенно при обмене данными с сервером или при попытке доступа к свойству нулевого объекта. Нам всегда приходится планировать худшее, и в большинстве случаев это делается черезtry-catchПойми.
Например:
const getUser = id =>
[{ id: 1, name: 'Loren' }, { id: 2, name: 'Zora' }]
.filter(x => x.id === id)[0]
const name = getUser(1).name
console.log(name) // => 'Loren'
const name2 = getUser(4).name
console.log(name2) // => 'TypeError: Cannot read property 'name' of undefined'
Итак, теперь код сообщает об ошибке, используйтеtry-catchЭта проблема может быть решена в некоторой степени:
try {
const result = getUser(4).name
console.log(result)
} catch (e) {
console.log('error', e.message) // => 'TypeError: Cannot read property 'name' of undefined'
}
Как только возникает ошибка, JavaScript немедленно прекращает выполнение и создает трассировку стека вызовов функции, вызвавшей проблему, и сохраняет ее в объекте Error.Catch является своего рода убежищем для нашего кода. ноtry-catchМожем ли мы правильно решить нашу проблему?try-catchСуществуют следующие недостатки:
- перерывПрозрачность котировокВ принципе, поскольку выдача исключения приведет к другому выходу из вызова функции, нельзя гарантировать единственное предсказуемое возвращаемое значение.
- вызоветпобочный эффект, так как исключения могут иметь непредсказуемые последствия для стека вне вызовов функций.
- нарушениепринцип локальности, так как код, используемый для восстановления после исключения, отличается от исходного вызова функции, а при возникновении ошибки функция покидает локальный стек и среду.
- Вы не можете просто заботиться о возвращаемом значении функции, вызывающая сторона должна нести ответственность за объявление типа сопоставления исключений в блоке catch для управления конкретными исключениями; это сложно комбинировать или связывать с другими функциями, и вы можете Не позволять следующей функции в конвейере обрабатывать то, что вызвала предыдущая функция.
- Происходит при наличии нескольких условий исключенияВложенные блоки обработки исключений.
Исключения должны создаваться в одном месте, а не везде.
Как видно из приведенного выше описания и кода,try-catchЭто полностью пассивное решение, и оно не очень «функционально», насколько хорошо было бы, если бы оно могло легко обрабатывать ошибки и даже терпеть их? Давайте воспользуемся концепцией Box, чтобы оптимизировать эти проблемы.
повернуть налево повернуть направо?
тщательный анализtry-catchЛогика блока кода обнаруживает, что выход нашего кода находится либо в попытке, либо в улове (функции не всегда могут иметь два возвращаемых значения). В соответствии с ожиданиями нашего дизайна кода, мы надеемся, что код будет завершен из ветки try, а catch — это наше восходящее решение, тогда мы можем провести аналог try asRightОтносится к обычной ветке, catchLeftОтносится к ветке, в которой возникает исключение, и они никогда не появятся одновременно! Итак, давайте расширим нашуBox, соответственноLeftа такжеRight, см. код:
const Left = x => ({
map: f => Left(x),
fold: (f, g) => f(x),
inspect: () => `Left(${x})`
})
const Right = x => ({
map: f => Right(f(x)),
fold: (f, g) => g(x),
inspect: () => `Right(${x})`
})
const resultLeft = Left(4).map(x => x + 1).map(x => x / 2)
console.log(resultLeft) // => Left(4)
const resultRight = Right(4).map(x => x + 1).map(x => x / 2)
console.log(resultRight) // => Right(2.5)
Leftа такжеRightРазница в том, что Left автоматически пропускаетmapФункция, передаваемая методом, и Right аналогична самой простой Box, которая будет выполнять функцию и переупаковывать возвращаемое значение в контейнер Right.Leftа такжеRightПрямо как в ОбещанииRejectа такжеResolve, результатом Promise является либо Reject, либо Resolve, а структуру с правой и левой ветвями мы можем назватьEither, либо влево, либо вправо, что нетрудно понять, правильно! Приведенный выше код иллюстрирует основное использование Left и Right, теперь поместите нашLeftа такжеRightприменимый кgetUserфункция!
const getUser = id => {
const user = [{ id: 1, name: 'Loren' }, { id: 2, name: 'Zora' }]
.filter(x => x.id === id)[0]
return user ? Right(user) : Left(null)
}
const result = getUser(4)
.map(x => x.name)
.fold(() => 'not found', x => x)
console.log(result) // => not found
невероятный! Теперь мы можем линейно обрабатывать ошибки и дажеnot foundнапоминание (предоставив его сбросить ), но подумайте еще раз, это наш оригинальныйgetUserфункция, которая может возвращатьundefinedИли нормальное значение, вы можете напрямую обернуть возвращаемое значение этой функции?
const fromNullable = x =>
x != null ? Right(x) : Left(null)
const getUser = id =>
fromNullable([{ id: 1, name: 'Loren' }, { id: 2, name: 'Zora' }]
.filter(x => x.id === id)[0])
const result = getUser(4)
.map(x => x.name)
.fold(() => 'not found', c => c.toUpperCase())
console.log(result) // => not found
Теперь, когда мы успешно разобрались с возможностью null или undefined, как насчет try-catch? Может ли он быть обернут Либо?
const tryCatch = (f) => {
try {
return Right(f())
} catch (e) {
return Left(e)
}
}
const jsonFormat = str => JSON.parse(str)
const app = (str) =>
tryCatch(() => jsonFormat(str))
.map(x => x.path)
.fold(() => 'default path', x => x)
const result = app('{"path":"some path..."}')
console.log(result) // => 'some path...'
const result2 = app('the way to death')
console.log(result2) // => 'default path'
Теперь наш try-catch не будет прерывать нашу композицию функций, даже если он сообщит об ошибке, и ошибка разумно контролируется, и объект Error не будет выброшен по желанию.
Здесь рекомендуется открыть NetEase Cloud Music для прослушивания песни"повернуть налево повернуть направо"! Кстати, успокойтесь и вспомните о наших правых и левых.
Что такое Functor?Как использовать Functor?Зачем использовать Functor?
Что такое функтор?
Выше мы определили простой Box, что на самом деле означает наличиеmapа такжеfoldТип метода. Давайте немного замедлимся и посмотрим и подумаем о нашемmap:Box(a) -> Box(b), по существу через функциюa -> bположить одинBox(a)карты наBox(b). Это похоже на знание функций в алгебре средней школы.Давайте рассмотрим определение функций в учебниках по алгебре:
Предполагая, что А и В являются двумя множествами, если согласно некоторому правилу соответствия любой элемент из А имеет единственный соответствующий ему элемент в В, то это соответствие называется функцией от множества А к множеству В.
Приведенный выше набор A и набор B, когда мы получаем наш программный мир, можно сравнить со строкой, логическим значением, числом и более абстрактным объектом, Обычно мы можем рассматривать тип данных как набор всех возможных значений (Set) . как Boolean можно рассматривать как[true,false]Набор , Число - это набор всех действительных чисел, все наборы с наборами в качестве объектов и отображением между наборами в виде стрелок составляют категорию:
Посмотрите на картинку: a, b и c представляют соответственно три категории Теперь проведем аналогию: a — набор строк (String), b — набор действительных чисел (Number), c — набор логических значений. ; тогда мы можем полностью реализовать функцию отображенияgдляstr => str.length, а функцияfдляnumber => number >=0 ? true : false, то мы можем передать функциюgЗавершите отображение из категории строк в категорию вещественных чисел, а затем передайте функциюfОтображение из категории вещественных чисел в категорию логических значений.
Теперь вернемся к неясному названию, которое мы пропустили ранее: Functor — это стрелка, которая переходит от категории к категории! И эта стрелка обычно сочетается с функцией преобразования через метод карты (т.е.str => str.length) реализовать, так что это легко понять, правильно (странный)
Если у нас есть функция g и функция f, то мы должны быть в состоянии вывести функциюh = f·g, то есть,const h = compose(f,g), а это нижняя половина изображения вышеa -> cПроцесс преобразования, это не математика средней школы结合律?Мы все изучали математику, кто бы не стал?
Подожди, что за чертовщина со стрелкой id на a, b, c? Сопоставление себя с собой? хорошо!
для любогоFunctor, через функциюconst id = x => xможет быть реализованfx.map(id) == id(fx), который называетсяIdentity, то есть по математике同一律.
Вот почему мы должны ввести теорию категорий, ввестиFunctorпонятие, а не просто называть ихmappaleИли что-то еще, потому что тогда мы сможем лучше понять другие теоремы Functor, которые сопровождают математику, сохраняя при этом то же имя (Compositionа такжеIdentity), не сдерживайте нас из-за этого неясного имени.
Вышеупомянутое введение предназначено только для удобства передовых отморозков (
По сравнению с Богом Хаскеля), чтобы понять категории в некоторой степени. не очень строго(очень неточно, ладно?), объект в категории не обязательно должен быть набором, стрелка не обязательно должна быть картой... СТОП! ! Останавливаться! После этого я могу сменить профессию и стать учителем алгебры (ahhhh.jpg).
Как пользоваться Функтором?
Теперь давайте снова вернемся в мир кода, без сомненияFunctorЭта концепция слишком распространена. На самом деле, подавляющее большинство разработчиков используютFunctorНо не осознавал этого. Например:
- Массив
mapа такжеfilter. - jQuery
cssа такжеstyle. - Обещания
thenа такжеcatchМетоды (Promise тоже функтор? Да!). - Rxjs наблюдаемый
mapа такжеfilter(Композиция асинхронных функций? Расслабьтесь!).
оба возвращают один и тот же типFunctor, так что его можно непрерывно вызывать в цепочке.По сути, это расширения концепции Box:
[1, 2, 3].map(x => x + 1).filter(x => x > 2)
$("#mybtn").css("width","100px").css("height","100px").css("background","red");
Promise.resolve(1).then(x => x + 1).then(x => x.toString())
Rx.Observable.fromEvent($input, 'keyup')
.map(e => e.target.value)
.filter(text => text.length > 0)
.debounceTime(100)
Зачем использовать Functor?
Поместите значение в контейнер (такой как Box, Right, Left и т. д.), а затем используйте толькоmapЧтобы управлять им, что это такое? Если мы изменим способ, ответ очевиден: какие преимущества принесет нам использование контейнера для использования функции? ответ:Абстракция, абстракция для использования функций.
Весь смысл функционального программирования заключается в объединении небольших функций в функции более высокого уровня.
В качестве примера композиции функций: если вы хотите дать какой-либоFunctorПрименить единую карту, как с этим быть? ответPartial Application:
const partial =
(fn, ...presetArgs) =>
(...laterArgs) =>
fn(...presetArgs, ...laterArgs);
const double = n => n * 2
const map = (fn, F) => F.map(fn)
const mapDouble = partial(map, double)
const res = mapDouble(Box(1)).fold(x => x)
console.log(res) // => 2
КлючmapDoubleРезультат, возвращаемый функцией, представляет собой функцию, ожидающую получения второго параметра F (Box(1)); как только второй параметр будет получен, она будет выполнена напрямую.F.map(fn), эквивалентноBox(1).map(double), результатом, возвращаемым этим выражением, являетсяBox(2), так что можете продолжать.foldИ так далее по цепочке операций.
Резюме и план
Суммировать
В приведенном выше примере представлены несколько основных концепций функционального программирования (чистая функция, компоновка) на примере Double Eleven Shopping Carnival и постепенно представлены мощные функциональные возможности.Boxпонятие, самое основноеFunctor. Позже, через ноль, который может появляться все время, вводитсяEitherМожет использоваться как нулевой контейнер. пройти сноваtry-catchНапример, я узнал о более чистом способе обработки ошибок.Конечно, «Либо» — это не только эти два варианта использования, и позже я продолжу знакомить вас с другими продвинутыми вариантами использования. Какой окончательный выводFunctor,как использоватьFunctor, и используяFunctorКаковы преимущества.
строить планы
FunctorЭто самая основная концепция в теории категорий, которую мы представили, но в настоящее время мы решаем самые простые проблемы (лучшая композиция (map), более надежный код (fromNullAble), более чистая обработка ошибок (TryCatch)), но как насчет вложенных try-catch? ? Как совместить асинхронные функции? будет проходить позже双11购物狂欢节的案例ввести другие понятия и примеры практического использования в теорию категорий (практическая цель: продолжить разоблачение рутины спекулянтов,Кстати, я сменил профессию на учителя алгебры и избавился от негласного правила исключения в 34 года; dog head.jpg).
Ссылки и цитируемые статьи:
- What is a functor?
- So You Want to be a Functional Programmer
- Two Years of Functional Programming in JavaScript: Lessons Learned
- Master the JavaScript Interview: What is Functional Programming?
- Руководство по функциональному программированию JavaScript
- «Ты не знаешь JS»
- Теория категорий для программистов
Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!