Функциональное программирование JavaScript (3)

контейнер JavaScript функциональное программирование Underscore.js

адрес слайда

В-четвертых, Talk дешево! Покажи мне деньги!

Следующее содержание в основном связано сProfessor Frisby Introduces Composable Functional JavaScript

show-me-the-money

4.1 Контейнер (коробка)

Предположим, есть функция, которая принимает строку чисел из пользовательского ввода. Нам нужно предварительно обработать его, удалить лишние пробелы, преобразовать в число и добавить единицу и, наконец, вернуть букву, соответствующую значению. Код выглядит так...

const nextCharForNumStr = (str) =>
  String.fromCharCode(parseInt(str.trim()) + 1)

nextCharForNumStr(' 64 ') // "A"

Из-за отсутствия мыслительного зала вложенность этого кода слишком компактна, я видел слишком много "старых и широких болей", поэтому я быстро его рефакторил...

五官太紧凑

const nextCharForNumStr = (str) => {
  const trimmed = str.trim()
  const number = parseInt(trimmed)
  const nextNumber = number + 1
  return String.fromCharCode(nextNumber)
}

nextCharForNumStr(' 64 ') // 'A'

Очевидно, что после предыдущего содержания (xi) Дао (нао) с первого взгляда видно, что эта пересмотренная версия кода не является Pointfree...

Для этих промежуточных переменных, которые используются только один раз, легко обдумывать или искать переводы, и их легко «пострадать» и изменить снова ~

老阔疼

const nextCharForNumStr = (str) => [str]
  .map(s => s.trim())
  .map(s => parseInt(s))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))

nextCharForNumStr(' 64 ') // ['A']

На этот раз с помощью метода карты массива мы разбили необходимые 4 шага на 4 маленькие функции.

Таким образом, вам больше не нужно думать об имени промежуточной переменной, и то, что делает каждый шаг, очень ясно, вы можете сразу увидеть, что делает этот код.

Мы помещаем исходную строковую переменную str в массив и становимся [str], что равносильно помещению ее в контейнер.

Код нравится, дверь~~?

稳

Но здесь мы можем пойти дальше и создать новый тип Box. Мы также определим метод карты и позволим ему делать то же самое.

const Box = (x) => ({
  map: f => Box(f(x)),        // 返回容器为了链式调用
  fold: f => f(x),            // 将元素从容器中取出
  inspect: () => `Box(${x})`, // 看容器里有啥
})

const nextCharForNumStr = (str) => Box(str)
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  .fold(c => c.toLowerCase()) // 可以轻易地继续调用新的函数

nextCharForNumStr(' 64 ') // a

Помимо создания контейнера, помимо прямой передачи параметров как функции, вы также можете использовать статические методыof.

По общему соглашению в функциональном программировании функторы имеют метод of, который используется для создания новых контейнеров.

Box(1) === Box.of(1)

На самом деле этоBoxявляется функтором, потому что он реализуетmapфункция. Конечно, вы также можете назвать этоMappableили другие имена.

Однако, чтобы сохранить название в соответствии с определением категоризации, мы стоим на плечах гигантов и не изобретаем новых существительных~ (различные странные существительные в следующих подразделах также произошли от математических существительных).

Функтор — это тип контейнера, который реализует функцию отображения и подчиняется определенным правилам.

Так что же это за конкретные правила?

  1. Правило первое:
fx.map(f).map(g) === fx.map(x => g(f(x)))

На самом деле это композиция функций...

  1. Правило второе:
const id = x => x

fx.map(id) === id(fx)

diagram-functor

4.2.Either / Maybe

cat

Предположим, теперь есть требование: получить шестнадцатеричное значение RGB соответствующего цвета и вернуться, чтобы удалить#после значения в верхнем регистре.

const findColor = (name) => ({
  red: '#ff4444',
  blue: '#3b5998',
  yellow: '#fff68f',
})[name]

const redColor = findColor('red')
  .slice(1)
  .toUpperCase() // FF4444

const greenColor = findColor('green')
  .slice(1)
  .toUpperCase()
// Uncaught TypeError:
// Cannot read property 'slice' of undefined

Приведенный выше код вводит существующий цветkeyОн отлично работает для значений, но выдает ошибку при передаче других цветов. Что я должен делать?

Не говоря уже об условном осуждении и всевозможных хитрых обработках ошибок. Давайте сначала посмотрим на функциональное решение~

Функциональность абстрагирует обработку ошибок отEitherконтейнер, и этот контейнер состоит из двух подконтейнеровRightа такжеLeftсочинение.

// Either 由 Right 和 Left 组成

const Left = (x) => ({
  map: f => Left(x),            // 忽略传入的 f 函数
  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 right = Right(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

right.inspect() // Right(14.5)
right.fold(e => 'error', x => x) // 14.5

const left = Left(4)
  .map(x => x * 7 + 1)
  .map(x => x / 2)

left.inspect() // Left(4)
left.fold(e => 'error', x => x) // error

Как можно заметитьRightа такжеLeftпохожий наBox:

  • Самая большая разница в том,foldфункция, здесь вам нужно передать две функции обратного вызова, та, что слева,Leftиспользовать, тот, что справа, даетRightиспользовать.
  • Во-вторых,LeftизmapФункция игнорирует пришедшую функцию (из-за ошибки, естественно, она не может продолжать выполняться).

Теперь вернемся к предыдущему вопросу~

const fromNullable = (x) => x == null
  ? Left(null)
  : Right(x)

const findColor = (name) => fromNullable(({
  red: '#ff4444',
  blue: '#3b5998',
  yellow: '#fff68f',
})[name])

findColor('green')
  .map(c => c.slice(1))
  .fold(
    e => 'no color',
    c => c.toUpperCase()
  ) // no color

Из приведенного выше кода я не знаю, видели ли читатели и господа использованиеEitherПреимущество этого в том, что вы можете безопасно выполнять любые операции с данными этого типа, вместо того, чтобы тщательно проверять параметры в каждой функции.

4.3.Chain / FlatMap / bind / >>=

Допустим, сейчас есть json-файл, в котором сохраняется порт. Мы хотим прочитать этот файл, чтобы получить порт. Если есть ошибка, значение по умолчанию — 3000.

// config.json
{ "port": 8888 }

// chain.js
const fs = require('fs')

const getPort = () => {
  try {
    const str = fs.readFileSync('config.json')
    const { port } = JSON.parse(str)
    return port
  } catch(e) {
    return 3000
  }
}

const result = getPort()

Так просто~, давайте воспользуемся Либо для рефакторинга, чтобы увидеть эффект.

const fs = require('fs')

const Left = (x) => ({ ... })
const Right = (x) => ({ ... })

const tryCatch = (f) => {
  try {
    return Right(f())
  } catch (e) {
    return Left(e)
  }
}

const getPort = () => tryCatch(
    () => fs.readFileSync('config.json')
  )
  .map(c => JSON.parse(c))
  .fold(e => 3000, c => c.port)

А, нормальная работа, хорошо выглядит~

Неплохой ты змееголов...!

Вышеприведенный код имеетbug,когдаjsonКогда возникает проблема с записью файла, вJSON.parseБудут возникать ошибки, поэтому этот шаг также необходимо использоватьtryCatchзаворачивать.

Но вот проблема...

Возвращаемое значение в это время может бытьRight(Right(''))илиRight(Left(e))(подумайте, почему бы и нетLeft(Right(''))илиLeft(Left(e))).

То есть то, что мы имеем сейчас, это двухслойный контейнер, как русская матрешка...

Чтобы получить значение из контейнера в контейнере, нам нужноfoldдважды...! (Если слоев больше...)

dog

Из-за отсутствия мыслительного зала умная и остроумная функциональная формула придумала новую цепочку методов~, которая на самом деле очень проста, то есть я знаю, что контейнер будет возвращен сюда, поэтому не используйте метод емкость для его упаковки.

...

const Left = (x) => ({
  ...
  chain: f => Left(x) // 和 map 一样,直接返回 Left
})

const Right = (x) => ({
  ...
  chain: f => f(x),   // 直接返回,不使用容器再包一层了
})

const tryCatch = (f) => { ... }

const getPort = () => tryCatch(
    () => fs.readFileSync('config.json')
  )
  .chain(c => tryCatch(() => JSON.parse(c))) // 使用 chain 和 tryCatch
  .fold(
    e => 3000,
    c => c.port
  )

На самом деле здесьLeftа такжеRightявляется монадой, потому что она реализуетchainфункция.

Монада — это тип контейнера, который реализует цепную функцию и подчиняется некоторым определенным правилам.

Прежде чем перейти к этим конкретным правилам, давайте определимjoinфункция:

// 这里的 m 指的是一种 Monad 实例
const join = m => m.chain(x => x)
  1. Правило первое:
join(m.map(join)) === join(join(m))
  1. Правило второе:
// 这里的 M 指的是一种 Monad 类型
join(M.of(m)) === join(m.map(M.of))

Это правило гласитmapвозможноchainа такжеofопределенный.

m.map(f) === m.chain(x => M.of(f(x)))

то естьMonadопределенно естьFunctor

MonadОчень мощный, и позже мы воспользуемся им для борьбы с различными побочными эффектами. Но не запутайся в этом,chainОсновная роль заключается в том, чтобы просто соединить два разных типа (join) вместе.

diagram-monad

4.4 Полугруппа

Определение 1. Для непустого множества S, если на S определена бинарная операция ○ такая, что для любых a, b ∈ S существует a ○ b ∈ S, то {S, ○} называется широкой группой .

Определение 2: Если {S, ○} — широкая группа, а операция ○ также удовлетворяет ассоциативному закону, то есть: для любых a, b, c ∈ S существует (a ○ b) ○ c = a ○ ( b ○ c), то назовем {S, ○} полугруппой.

Например, объекты с методом concat в JavaScript являются полугруппами.

// 字符串和 concat 是半群
'1'.concat('2').concat('3') === '1'.concat('2'.concat('3'))

// 数组和 concat 是半群
[1].concat([2]).concat([3]) === [1].concat([2].concat([3]))

Хотя теоретически для<Number, +>Другими словами, это соответствует определению полугруппы:

  • Добавление чисел по-прежнему возвращает числа (широкая группа)
  • Сложение удовлетворяет ассоциативному закону (полугруппа)

Но у чисел нет метода concat

ладно, пусть это произойдет<Number, +>составленный из полугруппы Sum.

const Sum = (x) => ({
  x,
  concat: ({ x: y }) => Sum(x + y), // 采用解构获取值
  inspect: () => `Sum(${x})`,
})

Sum(1)
  .concat(Sum(2))
  .inspect() // Sum(3)

Помимо,<Boolean, &&>Оно также удовлетворяет определению полугруппы~

const All = (x) => ({
  x,
  concat: ({ x: y }) => All(x && y), // 采用解构获取值
  inspect: () => `All(${x})`,
})

All(true)
  .concat(All(false))
  .inspect() // All(false)

Наконец, давайте создадим новую полугруппу First для строк, которая, как следует из названия, игнорирует все аргументы, кроме первого.

const First = (x) => ({
  x,
  concat: () => First(x), // 忽略后续的值
  inspect: () => `First(${x})`,
})

First('blah')
  .concat(First('yoyoyo'))
  .inspect() // First('blah')

да йо? Вы чувствуете, что эта полугруппа немного отличается от других полугрупп, но я не могу сказать, что это такое...?

Этот вопрос оставлен в следующем подразделе. Сначала позвольте мне сказать вам, для чего эта вещь.

const data1 = {
  name: 'steve',
  isPaid: true,
  points: 10,
  friends: ['jame'],
}
const data2 = {
  name: 'steve',
  isPaid: false,
  points: 2,
  friends: ['young'],
}

Предположим, есть два данных, которые нужно объединить, тогда с помощью полугруппы мы можем применить First к имени, All к isPaid, Sum к баллам, а последние друзья — это уже полугруппа...

const Sum = (x) => ({ ... })
const All = (x) => ({ ... })
const First = (x) => ({ ... })

const data1 = {
  name: First('steve'),
  isPaid: All(true),
  points: Sum(10),
  friends: ['jame'],
}
const data2 = {
  name: First('steve'),
  isPaid: All(false),
  points: Sum(2),
  friends: ['young'],
}

const concatObj = (obj1, obj2) => Object.entries(obj1)
  .map(([ key, val ]) => ({
    // concat 两个对象的值
    [key]: val.concat(obj2[key]),
  }))
  .reduce((acc, cur) => ({ ...acc, ...cur }))

concatObj(data1, data2)
/*
  {
    name: First('steve'),
    isPaid: All(false),
    points: Sum(12),
    friends: ['jame', 'young'],
  }
*/

4.5. Моноиды

Моноид — это полугруппа, в которой есть единичный элемент (моноид).

Все мы знаем о полугруппах, но что такое единичный элемент?

Единичный элемент: для полугруппы существует e ∈ S такое, что любой a ∈ S имеет a ○ e = e ○ a

Например, для полугруппы сложения чисел 0 является ее единичным элементом, поэтому<Number, +, 0>образуют моноид. По аналогии:

  • для<Number, *>Элементарная ячейка равна 1
  • для<Boolean, &&>С точки зрения элементарной ячейки верно
  • для<Boolean, ||>Это ложный элемент идентификации
  • для<Number, Min>Единица измерения - бесконечность
  • для<Number, Max>Элементарная ячейка - бесконечность

Так<String, First>Это моноид?

очевидно мыНеНайдите такой элемент идентичности e, который удовлетворяет

First(e).concat(First('steve')) === First('steve').concat(First(e))

Это небольшая интрига, оставленная в предыдущем разделе, почему кажется, что «Первое» не то же самое, что «Сумма и все».

Какая конкретная разница между ними?

На самом деле, первая реакция на моноид должна бытьзначение по умолчанию или начальное значение, например, второй параметр функции сокращения должен передавать начальное значение или значение по умолчанию.

// sum
const Sum = (x) => ({ ... })
Sum.empty = () => Sum(0) // 单位元

const sum = xs => xs.reduce((acc, cur) => acc + cur, 0)

sum([1, 2, 3])  // 6
sum([])         // 0,而不是报错!

// all
const All = (x) => ({ ... })
All.empty = () => All(true) // 单位元

const all = xs => xs.reduce((acc, cur) => acc && cur, true)

all([true, false, true]) // false
all([])                  // true,而不是报错!

// first
const First = (x) => ({ ... })

const first = xs => xs.reduce(acc, cur) => acc)

first(['steve', 'jame', 'young']) // steve
first([])                         // boom!!!

Из приведенного выше кода видно, что моноиды лучше, чем полугруппы.Безопасностьгораздо более,

4.6.foldMap

1. Рутины

В коде для использования моноидов в предыдущем разделе, если вы передаете экземпляры моноидов вместо примитивных типов, вы обнаружите, что это на самом деле рутина...

const Monoid = (x) => ({ ... })

const monoid = xs => xs.reduce(
    (acc, cur) => acc.concat(cur),  // 使用 concat 结合
    Monoid.empty()                  // 传入幺元
)

monoid([Monoid(a), Monoid(b), Monoid(c)]) // 传入幺半群实例

Поэтому для функционального типа с высоко абстрактным мышлением такой код нужно продолжать рефакторить и упрощать~

2. Список, карта

Прежде чем объяснять, как проводить рефакторинг, давайте сначала представим две неизменяемые структуры данных, обычно используемые в жареной курице:List,Map.

Как следует из названия, он соответствует родномуArrayа такжеObject.

3. Рефакторинг с использованием List и Map

потому чтоimmutableв библиотекеListа такжеMapнисколькоemptyсвойства иfoldметод, поэтому мы сначала расширяем List и Map~

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.reduce((acc, cur) => acc.concat(cur), empty)
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold

// from https://github.com/DrBoolean/immutable-ext

Таким образом, код из предыдущего раздела можно упростить до следующего:

List.of(1, 2, 3)
  .map(Sum)
  .fold(Sum.empty())     // Sum(6)

List().fold(Sum.empty()) // Sum(0)

Map({ steve: 1, young: 3 })
  .map(Sum)
  .fold(Sum.empty())     // Sum(4)

Map().fold(Sum.empty())  // Sum(0)

4. Рефакторинг с помощью foldMap

уведомлениеmapа такжеfoldЭти два шага логически являются одной операцией, поэтому мы можем добавитьfoldMapспособ совмещения двух.

import { List, Map } from 'immutable'

const derived = {
  fold (empty) {
    return this.foldMap(x => x, empty)
  },
  foldMap (f, empty) {
    return empty != null
      // 幺半群中将 f 的调用放在 reduce 中,提高效率
      ? this.reduce(
          (acc, cur, idx) =>
            acc.concat(f(cur, idx)),
          empty
      )
      : this
        // 在 map 中调用 f 是因为考虑到空的情况
        .map(f)
        .reduce((acc, cur) => acc.concat(cur))
  },
}

List.prototype.empty = List()
List.prototype.fold = derived.fold
List.prototype.foldMap = derived.foldMap

Map.prototype.empty = Map({})
Map.prototype.fold = derived.fold
Map.prototype.foldMap = derived.foldMap

// from https://github.com/DrBoolean/immutable-ext

Итак, окончательный вариант выглядит так:

List.of(1, 2, 3)
  .foldMap(Sum, Sum.empty()) // Sum(6)
List()
  .foldMap(Sum, Sum.empty()) // Sum(0)

Map({ a: 1, b: 3 })
  .foldMap(Sum, Sum.empty()) // Sum(4)
Map()
  .foldMap(Sum, Sum.empty()) // Sum(0)

4.7.LazyBox

Далее мы собираемся реализовать новый контейнерLazyBox.

Как следует из названия, этот контейнер ленив...

Хотя вы можете продолжать использоватьmapставь ему задачи, но пока ты не звонишьfoldметод побуждает его к выполнению (например,deadlineто же самое), он просто не выполняется...

const LazyBox = (g) => ({
  map: f => LazyBox(() => f(g())),
  fold: f => f(g()),
})

const result = LazyBox(() => ' 64 ')
  .map(s => s.trim())
  .map(i => parseInt(i))
  .map(i => i + 1)
  .map(i => String.fromCharCode(i))
  // 没有 fold 死活不执行

result.fold(c => c.toLowerCase()) // a

4.8.Task

1. Основное введение

С предыдущим разделомLazyBoxПосле основ давайте создадим новый типTask。

первыйTaskМожет получить функцию-конструктор для расчета задержки, конечно, тоже можно использоватьofметод создания экземпляра, естественно иметьmap,chain,concat,emptyи другие методы.

Разница в том, что у него естьforkМетод (похоже наLazyBoxсерединаfoldметод, вforkдругие функции не выполняются до выполнения), аrejectedметод, аналогичныйLeft, игнорируя последующие операции.

import Task from 'data.task'

const showErr = e => console.log(`err: ${e}`)
const showSuc = x => console.log(`suc: ${x}`)

Task
  .of(1)
  .fork(showErr, showSuc) // suc: 1

Task
  .of(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // suc: 2

// 类似 Left
Task
  .rejected(1)
  .map(x => x + 1)
  .fork(showErr, showSuc) // err: 1

Task
  .of(1)
  .chain(x => new Task.of(x + 1))
  .fork(showErr, showSuc) // suc: 2

2. Пример использования

Далее, давайте сделаем программу для запуска ракет~

const lauchMissiles = () => (
  // 和 promise 很像,不过 promise 会立即执行
  // 而且参数的位置也相反
  new Task((rej, res) => {
    console.log('lauchMissiles')
    res('missile')
  })
)

// 继续对之前的任务添加后续操作(duang~给飞弹加特技!)
const app = lauchMissiles()
  .map(x => x + '!')

// 这时才执行(发射飞弹)
app.fork(showErr, showSuc)

3. Принципиальное значение

Приведенный выше код кажется на первый взгляд бесполезным, но он просто оборачивает код, который должен быть выполнен, с помощью функции, может ли это все еще быть унесено в небо?

Помните побочные эффекты, упомянутые в предыдущей главе? Хотя использование чистых функций не имеет побочных эффектов, существуют различные побочные эффекты, с которыми приходится иметь дело в повседневных проектах.

Итак, после того, как мы обернем код побочными эффектами, эти новые функции станут чистыми функциями, так что код всего нашего приложения будет чистым ~, и до фактического выполнения кода (forkраньше) также может постоянноcomposeДругие функции продолжают добавлять различные функции в наше приложение, так что поток кода всего приложения будет очень лаконичным и красивым.

side-effects

4. Пример асинхронного вложения

Следующий код делает 3 вещи:

  1. Прочитайте данные в config1.json
  2. Замените 8 в содержании на 6
  3. Запишите новое содержимое в config2.json.
import fs from 'fs'

const app = () => (
  fs.readFile('config1.json', 'utf-8', (err, contents) => {
    if (err) throw err

    const newContents = content.replace(/8/g, '6')

    fs.writeFile('config2.json', newContents, (err, _) => {
      if (err) throw err

      console.log('success!')
    })
  })
)

Давайте перепишем это с помощью Task~

import fs from 'fs'
import Task from 'data.task'

const cfg1 = 'config1.json'
const cfg2 = 'config2.json'

const readFile = (file, enc) => (
  new Task((rej, res) =>
    fs.readFile(file, enc, (err, str) =>
      err ? rej(err) : res(str)
    )
  )
)

const writeFile = (file, str) => (
  new Task((rej, res) =>
    fs.writeFile(file, str, (err, suc) =>
      err ? rej(err) : res(suc)
    )
  )
)

const app = readFile(cfg1, 'utf-8')
  .map(str => str.replace(/8/g, '6'))
  .chain(str => writeFile(cfg2, str))

app.fork(
  e => console.log(`err: ${e}`),
  x => console.log(`suc: ${x}`)
)

Код понятен с первого взгляда, задачи выполняются в линейной последовательности, а требования можно вставлять или изменять по желанию~

4.9.Applicative Functor

1. Введение проблемы

Applicative FunctorПредоставляет возможность применять разные функторы друг к другу.

Зачем нам взаимное применение функторов? Что такое взаимное применение?

Давайте рассмотрим простой пример:

const add = x => y => x + y

add(Box.of(2))(Box.of(3)) // NaN

Box(2).map(add).inspect() // Box(y => 2 + y)

Теперь у нас есть контейнер, внутреннее значение которого — частично примененная функция. Затем мы хотим применить его кBox(3)вверх и, наконец, получитьBox(5)ожидаемый результат.

Говоря о ценности из контейнера, это, безусловно, первая мысльchainспособ, попробуем:

Box(2)
  .chain(x => Box(3).map(add(x)))
  .inspect() // Box(5)

Успешная реализация~, НО, есть проблема с этим методом реализации, то есть с монадойисполнительный листвопрос.

Если мы это сделаем, нам придется подождатьBox(2)После завершения выполнения,Box(3)оценить. Если это две асинхронные задачи, их вообще нельзя выполнять параллельно.

Не паникуй, прими лекарство~

2. Основное введение

Вот главные герои:ap~:

const Box = (x) => ({
  // 这里 box 是另一个 Box 的实例,x 是函数
  ap: box => box.map(x),
  ...
})

Box(add)
  // Box(y => 2 + y) ,咦?在哪儿见过?
  .ap(Box(2))
  .ap(Box(3)) // Box(5)

алгоритм

F(x).map(f) === F(f).ap(F(x))

// 这就是为什么
Box(2).map(add) === Box(add).ap(Box(2))

3. Поднимите семью

Так как при непосредственном использовании ap при написании кода получается слишком много шаблонного кода, его обычно упрощают, используя семейство функций Lift.

// F 该从哪儿来?
const fakeLiftA2 = f => fx => fy => F(f).ap(fx).ap(fy)

// 应用运算规则转换一下~
const liftA2 = f => fx => fy => fx.map(f).ap(fy)

liftA2(add, Box(2), Box(4)) // Box(6)

// 同理
const liftA3 = f => fx => fy => fz => fx.map(f).ap(fy).ap(fz)
const liftA4 = ...
...
const liftAN = ...

4. Поднять приложение

  • пример 1
// 假装是个 jQuery 接口~
const $ = selector =>
  Either.of({ selector, height: 10 })

const getScreenSize = screen => head => foot =>
  screen - (head.height + foot.height)

liftA2(getScreenSize(800))($('header'))($('footer')) // Right(780)
  • Пример 2
// List 的笛卡尔乘积
List.of(x => y => z => [x, y, z].join('-'))
  .ap(List.of('tshirt', 'sweater'))
  .ap(List.of('white', 'black'))
  .ap(List.of('small', 'medium', 'large'))
  • Пример 3
const Db = ({
  find: (id, cb) =>
    new Task((rej, res) =>
      setTimeout(() => res({ id, title: `${id}`}), 100)
    )
})

const reportHeader = (p1, p2) =>
  `Report: ${p1.title} compared to ${p2.title}`

Task.of(p1 => p2 => reportHeader(p1, p2))
  .ap(Db.find(20))
  .ap(Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8

liftA2
  (p1 => p2 => reportHeader(p1, p2))
  (Db.find(20))
  (Db.find(8))
  .fork(console.error, console.log) // Report: 20 compared to 8

4.10.Traversable

1. Введение проблемы

import fs from 'fs'

// 详见 4.8.
const readFile = (file, enc) => (
  new Task((rej, res) => ...)
)

const files = ['a.js', 'b.js']

// [Task, Task],我们得到了一个 Task 的数组
files.map(file => readFile(file, 'utf-8'))

Однако нам нужен массив, содержащий массивTask([file1, file2])Это можно назватьforkметод для просмотра результата выполнения.

Для решения этой проблемы функциональное программирование обычно использует метод, называемыйtraverseметод достижения.

files
  .traverse(Task.of, file => readFile(file, 'utf-8'))
  .fork(console.error, console.log)

traverseПервый параметр метода — это функция, которая создает функтор, а второй параметр — это функция, которая будет применена к функтору.

2. Осознайте

На самом деле приведенный выше код имеетbug... потому что массив Array неtraverseметод. Все в порядке, давайте сделаем это~

Array.prototype.empty = []

// traversable
Array.prototype.traverse = function (point, fn) {
  return this.reduce(
    (acc, cur) => acc
      .map(z => y => z.concat(y))
      .ap(fn(cur)),
    point(this.empty)
  )
}

Глядя немного головокружение?

Не торопитесь, сначала посмотрите на тело кодаreduce, это очень знакомо, это обход элементов слева направо, а второй переданный параметр — это единичный элемент (пустой) моноида.

Посмотрите на первый параметр, в основном черезapplicative functorпередачаapметод, а затем использовать результат его выполнения для использованияconcatметоды объединяются в массив.

Таким образом, окончательный возвратTask([foo, bar]), поэтому мы можем позвонитьforkметод его выполнения.

4.11 Естественные преобразования

1. Основные понятия

Естественное преобразование — это функция, которая принимает функтор и возвращает другой функтор. Взгляните на код, чтобы ознакомиться с ним~

const boxToEither = b => b.fold(Right)

этоboxToEitherФункция — это естественное преобразование (nt), которое преобразует функторBoxПреобразован в другой функторEither.

тогда мы используемLeftЭто будет работать?

Ответ - нет!

Поскольку естественное преобразование — это не просто преобразование одного функтора в другой, оно также удовлетворяет следующим правилам:

nt(x).map(f) == nt(x.map(f))

natural_transformation

Например:

const res1 = boxToEither(Box(100))
  .map(x => x * 2)
const res2 = boxToEither(
  Box(100).map(x => x * 2)
)

res1 === res2 // Right(200)

первый функторaвнесите изменение и преобразуйте его в функторb, эквивалентен функтору firstaпреобразовать в функторbВнесите еще одно изменение.

Очевидно,Leftне соответствует этому правилу. Таким образом, любая функция, удовлетворяющая этому правилу,естественная трансформация.

2. Сценарии применения

1. Пример 1: получить значение, которое в два раза больше последнего числа массива меньше или равно 100

const arr = [2, 400, 5, 1000]
const first = xs => fromNullable(xs[0])
const double = x => x * 2
const getLargeNums = xs => xs.filter(x => x > 100)

first(
  getLargeNums(arr).map(double)
)

Согласно естественному преобразованию, это, очевидно, то же самое, что иfirst(getLargeNums(arr)).map(double)эквивалентны. Но последний явно лучше.

Давайте рассмотрим немного более сложный пример:

2. Пример 2: Найти id лучшего друга пользователя с id 3

// 假 api
const fakeApi = (id) => ({
  id,
  name: 'user1',
  bestFriendId: id + 1,
})

// 假 Db
const Db = {
  find: (id) => new Task(
    (rej, res) => (
      res(id > 2
        ? Right(fakeApi(id))
        : Left('not found')
      )
    )
  )
}
// Task(Either(user))
const zero = Db.find(3)

// 第一版
// Task(Either(Task(Either(user)))) ???
const one = zero
  .map(either => either
    .map(user => Db
      .find(user.bestFriendId)
    )
  )
  .fork(
    console.error,
    either => either // Either(Task(Either(user)))
      .map(t => t.fork( // Task(Either(user))
        console.error,
        either => either
            .map(console.log), // Either(user)
      ))
  )

黑人问号4合一

Что за чертовщина? ? ?

Так точно нельзя...

// Task(Either(user))
const zero = Db.find(3)

// 第二版
const two = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
    .chain(user => Db
      .find(user.bestFriendId) // Task(Either(user))
    )
    .chain(either => either
      .fold(Task.rejected, Task.of) // Task(user)
    )
  )
  .fork(
    console.error,
    console.log,
  )

Проблема второй версии — избыточный вложенный код.

// Task(Either(user))
const zero = Db.find(3)

// 第三版
const three = zero
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(either => either
    .fold(Task.rejected, Task.of) // Task(user)
  )
  .fork(
    console.error,
    console.log,
  )

Проблема с третьим изданием заключается в избыточной логике.

// Task(Either(user))
const zero = Db.find(3)

// 这其实就是自然变换
// 将 Either 变换成 Task
const eitherToTask = (e) => (
  e.fold(Task.rejected, Task.of)
)

// 第四版
const four = zero
  .chain(eitherToTask) // Task(user)
  .chain(user => Db
    .find(user.bestFriendId) // Task(Either(user))
  )
  .chain(eitherToTask) // Task(user)
  .fork(
    console.error,
    console.log,
  )

// 出错版
const error = Db.find(2) // Task(Either(user))
  // Task.rejected('not found')
  .chain(eitherToTask)
  // 这里永远不会被调用,被跳过了
  .chain(() => console.log('hey man'))
  ...
  .fork(
    console.error, // not found
    console.log,
  )

4.12 Изоморфизм

Изоморфизм — это класс отображений, определенных между математическими объектами, которые раскрывают отношения, существующие между свойствами или операциями этих объектов.

Проще говоря, два разных типа объектов деформируются, сохраняя структуру и не теряя данных.

Как это сделать?

Фактически изоморфизм — это пара функций:toа такжеfrom, соблюдать следующие правила:

to(from(x)) === x
from(to(y)) === y

Это на самом деле показывает, что оба типа могут хранить одну и ту же информацию без потерь.

1. НапримерStringа также[Char]изоморфен.

// String ~ [Char]
const Iso = (to, from) => ({ to, from })

const chars = Iso(
  s => s.split(''),
  c => c.join('')
)

const str = 'hello world'

chars.from(chars.to(str)) === str

Что это может сделать?

const truncate = (str) => (
  chars.from(
    // 我们先用 to 方法将其转成数组
    // 这样就能使用数组的各类方法
    chars.to(str).slice(0, 3)
  ).concat('...')
)

truncate(str) // hel...

2. Посмотрим на массив не более чем с одним параметром[a]а такжеEitherТакой же

// [a] ~ Either null a
const singleton = Iso(
  e => e.fold(() => [], x => [x]),
  ([ x ]) => x ? Right(x) : Left()
)

const filterEither = (e, pred) => singleton
  .from(
    singleton
      .to(e)
      .filter(pred)
  )

const getUCH = (str) => filterEither(
  Right(str),
  x => x.match(/h/ig)
).map(x => x.toUpperCase())

getUCH('hello') // Right(HELLO)

getUCH('ello') // Left(undefined)

использованная литература

Статьи по Теме

выше продолжение следует...