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

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

Автор этой статьи:Чжао Сянтао

Концепция функционального программирования (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.
  • jQuerycssа также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).

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

Эта статья была опубликована сКоманда внешнего интерфейса NetEase Cloud Music, Любое несанкционированное воспроизведение статьи запрещено. Мы всегда нанимаем, если вы готовы сменить работу и вам нравится облачная музыка, тоПрисоединяйтесь к нам!