Автор этой статьи:Чжао Сянтао
Концепция функционального программирования (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, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!