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

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

адрес слайда

В-третьих, да, это очень функционально~

this-is-very-fp

3.1 Функции — это граждане первого сорта!

3.1.1 Злоупотребление анонимными функциями

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

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

// 太傻了
const getServerStuff = function (callback) {
  return ajaxCall(function (json) {
    return callback(json)
  })
}

// 这才像样
const getServerStuff = ajaxCall

// 下面来推导一下...
const getServerStuff
  === callback => ajaxCall(json => callback(json))
  === callback => ajaxCall(callback)
  === ajaxCall

// from JS函数式编程指南

Давайте посмотрим на другой пример...

const BlogController = (function () {
  const index = function (posts) {
    return Views.index(posts)
  }

  const show = function (post) {
    return Views.show(post)
  }

  const create = function (attrs) {
    return Db.create(attrs)
  }

  const update = function (post, attrs) {
    return Db.update(post, attrs)
  }

  const destroy = function (post) {
    return Db.destroy(post)
  }

  return { index, show, create, update, destroy }
})()

// 以上代码 99% 都是多余的...

const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy,
}

// ...或者直接全部删掉
// 因为它的作用仅仅就是把视图(Views)和数据库(Db)打包在一起而已。

// from JS函数式编程指南

3.1.2 За что вы любите первоклассных граждан?

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

// 原始函数
httpGet('/post/2', function (json) {
  return renderPost(json)
})

// 假如需要多传递一个 err 参数
httpGet('/post/2', function (json, err) {
  return renderPost(json, err)
})

// renderPost 将会在 httpGet 中调用,
// 想要多少参数,想怎么改都行
httpGet('/post/2', renderPost)

3.1.3. Улучшить коэффициент повторного использования функций

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

// 只针对当前的博客
const validArticles = function (articles) {
  return articles.filter(function (article) {
    return article !== null && article !== undefined
  })
}

// 通用性好太多
const compact = function(xs) {
  return xs.filter(function (x) {
    return x !== null && x !== undefined
  })
}

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

3.1.4.this

this-js

В функциональном программировании вы вообще не используете это...

Но это не значит, что нужно избегать использования этого(Цзян Лай сообщил об отклонении... Вы знаете об этом?)

3.2. Каррирование

3.2.1 Концепция каррирования

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

import { curry } from 'lodash'

const add = (x, y) => x + y
const curriedAdd = curry(add)

const increment = curriedAdd(1)
const addTen = curriedAdd(10)

increment(2) // 3
addTen(2) // 12

Карри был назван Кристофером Стрейчи в честь логика Хаскелла Карри. Конечно, язык программирования Haskell также произошел от его имени. Хотя карри изобрели Мозес Шнфинкель и Готтлоб Фреге.

3.2.2 Карринг против частичного применения

In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity.

by wikipedia

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

Каррирование обычно происходит вместе с применением частичных функций, но это два разных понятия:

import { curry, partial } from 'lodash'

const add = (x, y, z) => x + y + z

const curriedAdd = curry(add)       // <- 只接受一个函数

const addThree = partial(add, 1, 2) // <- 不仅接受函数,还接受至少一个参数
  === curriedAdd(1)(2)              // <- 柯里化每次都返回一个单参函数

Проще говоря, многопараметрическая функция (n-арная) становится n*1-ичной после каррирования, а частичная функция становится (n-x)-арной после применения x параметров

3.2.3 Реализация каррирования

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

// 实现一个函数 curry 满足以下调用、
const f = (a, b, c, d) => { ... }
const curried = curry(f)

curried(a, b, c, d)
curried(a, b, c)(d)
curried(a)(b, c, d)
curried(a, b)(c, d)
curried(a)(b, c)(d)
curried(a)(b)(c, d)
curried(a, b)(c)(d)

Очевидной первой реакцией является использование рекурсии, чтобы можно было вернуть ряд функций. Конечным условием рекурсии является принятие количества параметров исходной функции, поэтому основное внимание уделяется передаче параметров~

// ES5
var curry = function curry (fn, arr) {
  arr = arr || []

  return function () {
    var args = [].slice.call(arguments)
    var arg = arr.concat(args)

    return arg.length >= fn.length
      ? fn.apply(null, arg)
      : curry(fn, arg)
  }
}

// ES6
const curry = (fn, arr = []) => (...args) => (
  arg => arg.length >= fn.length
    ? fn(...arg)
    : curry(fn, arg)
)([...arr, ...args])

3.2.4. Значение каррирования

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

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

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

// 定义通用函数
const converter = (
  toUnit,
  factor,
  offset = 0,
  input
) => ([
  ((offset + input) * factor).toFixed(2),
  toUnit,
].join(' '))

// 分别绑定不同参数
const milesToKm =
  curry(converter)('km', 1.60936, undefined)
const poundsToKg =
  curry(converter)('kg', 0.45460, undefined)
const farenheitToCelsius =
  curry(converter)('degrees C', 0.5556, -32)

-- from https://stackoverflow.com/a/6861858

Вы можете возразить, что на самом деле вы не можете использовать эти причудливые каррирования, приложения с частичной функцией и т. Д., Я могу реализовать описанную выше логику напрямую с железным ребенком. (Этот худой, не будем об этом...)

function converter (ratio, symbol, input) {
  return (input * ratio).toFixed(2) + ' ' + symbol
}

converter(2.2, 'lbs', 4)
converter(1.62, 'km', 34)
converter(1.98, 'US pints', 2.4)
converter(1.75, 'imperial pints', 2.4)

-- from https://stackoverflow.com/a/32379766

Разница, однако, в том, что если функцияconverterТребуемые параметры нельзя получить одновременно, и это не влияет на способ каррирования, потому что известные параметры уже сохранены при замыкании. Последнее может потребовать использования переменной стадии или других методов дляГарантия получения всех параметров одновременно.

3.3 Композиция функций (compose)

3.3.1 Понятие композиции

Композиция функций — это комбинация двух или более функций для формирования новой функции.

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

Звучит загадочно, но на самом деле предполагается, что есть функция f и еще одна функция g, а также данные x, а конечным результатом после вычисления является f(g(x)).

composition-of-functions

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

Если y является функцией от w, а w является функцией от x, то есть y = f(w), w = g(x), то функция y = f[g(x)] от y относительно x равна называется функцией y = f (составная функция w) и w = g (x). где w — промежуточная переменная, x — независимая переменная, y — значение функции.

Кроме того, из дискретной математики вы должны были знать, что составная функция f(g(h(x))) может быть записана как (f ○ g ○ h)(x). (На самом деле это композиция функций)

3.3.2 Реализация композиции

function-composition

const add1 = x => x + 1
const mul3 = x => x * 3
const div2 = x => x / 2

div2(mul3(add1(add1(0)))) // 结果是 3,但这样写可读性太差了

const operate = compose(div2, mul3, add1, add1)
operate(0) // => 相当于 div2(mul3(add1(add1(0))))
operate(2) // => 相当于 div2(mul3(add1(add1(2))))

// redux 版
const compose = (...fns) => {
  if (fns.length === 0) return arg => arg
  if (fns.length === 1) return fns[0]

  return fns.reduce((a, b) => (...args) => a(b(...args)))
}

// 一行版,支持多参数,但必须至少传一个函数
const compose = (...fns) => fns.reduceRight((acc, fn) => (...args) => fn(acc(...args)))

// 一行版,只支持单参数,但支持不传函数
const compose = (...fns) => arg => fns.reduceRight((acc, fn) => fn(acc), arg)

3.3.3.Pointfree

Придумать имя — хлопотно,PointfreeСтиль может эффективно сократить наименования большого количества промежуточных переменных.

Pointfree означает, что обрабатываемое значение не используется, а синтезируется только процесс операции. Китайский можно перевести как «бесполезный» стиль.

from Руководство по стилю бесточечного программирования

См. пример ниже. (обратите внимание на концепцию понимания функций как первоклассных граждан и композицию функций)

const addOne = x => x + 1
const square = x => x * x

Выше приведены две простые функцииaddOneа такжеsquare, теперь объедините их в одну операцию.

const addOneThenSquare = compose(square, addOne)
addOneThenSquare(2) //  9

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

// 非 Pointfree,因为提到了数据:word
const snakeCase = function (word) {
  return word.toLowerCase().replace(/\s+/ig, '_')
}

// Pointfree
const snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase)

Однако, к сожалению, вышеизложенноеPointfreeвыдаст ошибку, потому что в JavaScriptreplaceа такжеtoLowerCaseфункция определена вStringВ цепочке прототипов...

Кроме того, некоторые библиотеки (например, Underscore, Lodash...) помещают обрабатываемые данные в первый параметр.

const square = n => n * n;

_.map([4, 8], square) // 第一个参数是待处理数据

R.map(square, [4, 8]) // 一般函数式库都将数据放在最后

Это приводит к некоторым очень нефункциональным проблемам, а именно:

1. Невозможно каррировать приложение частичной функции

2. Невозможно выполнить композицию функций

3. Невозможно расширить карту (уменьшить и другие методы) до различных других типов.

(Подробности см. в разделе «Эй, подчеркивание, вы делаете это неправильно!» в разделе «Ссылки»).

3.3.4 Значение функциональной композиции

Сначала давайте подумаем об этом на уровне абстракции: из чего состоит приложение? (Конечно, оно состоит из трех букв: а, р и р.)

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

start-transform-effect

  • Начало может быть:

    • Откройте приложение
    • События DOM (DOMContentLoaded, onClick, onSubmit...)
    • Получен HTTP-запрос
    • Возвращенный HTTP-ответ
    • Результат запроса к базе
    • Сообщения WebSocket
    • ..
  • Конец или эффект может быть:

    • Рендеринг или обновление пользовательского интерфейса
    • Инициировать событие DOM
    • Создать HTTP-запрос
    • вернуть HTTP-ответ
    • сохранить данные в БД
    • Отправлять сообщения WebSocket
    • ...

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

эти глаголы описывают эти преобразованиясделал что-то(вместоКак это сделать)Такие как:

  • filter
  • slice
  • map
  • reduce
  • concat
  • zip
  • fork
  • flatten
  • ...

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

transformation-1
transformation-2
transformation-3
transformation-4

И если эти преобразования написаны в соответствии с основными функциональными правилами и лучшими практиками (чистые функции, отсутствие побочных эффектов, ссылочная прозрачность...).

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

3.4 Сигнатуры типа Хиндли-Милнера

3.4.1 Основные понятия

Давайте сначала рассмотрим несколько примеров~

// strLength :: String -> Number
const strLength = s => s.length

// join :: String -> [String] -> String
const join = curry((what, xs) => xs.join(what))

// match :: Regex -> String -> [String]
const match = curry((reg, s) => s.match(reg))

// replace :: Regex -> String -> String -> String
const replace = curry((reg, sub, s) => s.replace(reg, sub))

В системе Хиндли-Милнера функции записываются как a -> b, где a и b — переменные любого типа.

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

Например дляmatchФункции, после того как мы их каррируем, могут группировать сигнатуры типов следующим образом:

// match :: Regex -> (String -> [String])
const match = curry((reg, s) => s.match(reg))

Теперь мы можем видетьmatchЭта функция сначала принимаетRegexВ качестве параметра возвращаетStringприбыть[String]Функция.

Из-за каррирования результат такой:matchфункция одинRegexпараметры, вы получаете новую функцию, которая затем может обрабатыватьStringпараметр.

Предположим, мы передаем первый параметр/holiday/ig, то код становится таким:

// match :: Regex -> (String -> [String])
const match = curry((reg, s) => s.match(reg))

// onHoliday :: String -> [String]
const onHoliday = match(/holiday/ig)

Можно видеть, что каждый раз, когда параметр передается после каррирования, всплывает тип, стоящий перед сигнатурой типа. такonHolidayон уже естьRegexпараметрическийmatchфункция.

// replace :: Regex -> (String -> (String -> String))
const replace = curry((reg, sub, s) => s.replace(reg, sub))

Таким же образом посмотрите на последнюю функциюreplace, видно чтоreplaceДобавление такого количества скобок немного излишне.

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

Давайте рассмотрим еще несколько примеров~

//  id :: a -> a
const id = x => x

//  map :: (a -> b) -> [a] -> [b]
const map = curry((f, xs) => xs.map(f))

Функция id здесь принимает любой тип a и возвращает тот же тип данных (почему в сигнатуру map~ добавлены круглые скобки).

Как и в обычном коде, мы также можем использовать переменные в сигнатурах типов. Именование переменных a и b — это просто соглашение, вы можете использовать все, что захотите. Но для одного и того же имени переменной ее тип должен быть одинаковым.

Это очень важный принцип, поэтому мы должны повторить: a -> b может быть от любого типа a до любого типа b, но a -> a должен быть одного и того же типа.

Например, id может быть String -> String или Number -> Number, но не String -> Bool.

Точно так же карта использует переменные, за исключением того, что b может быть или не быть того же типа, что и a .

Мы можем понять это так: map принимает два параметра, первый — это функция из любого типа a в любой тип b, второй — массив, элементы которого — любой тип a, окончательный результат map — массив типа b. .

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

Наконец, еще несколько сложных примеров~

//  head :: [a] -> a
const head = xs => xs[0]

//  filter :: (a -> Bool) -> [a] -> [a]
const filter = curry((f, xs) => xs.filter(f))

//  reduce :: (b -> a -> b) -> b -> [a] -> b
const reduce = curry((f, x, xs) => xs.reduce(f, x))

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

Точно правильная интерпретация не гарантируется...

Глядя на сигнатуру сокращения, вы можете видеть, что его первый аргумент — это функция (отсюда и скобки), которая принимает b и a и возвращает b.

Так откуда же взялись эти a и b?

Проще говоря, второй и третий аргументы в сигнатуре — это b и массив элементов a , поэтому единственное разумное предположение здесь состоит в том, что b и каждый a будут переданы функции в качестве аргументов. Мы также можем видеть, что конечным результатом, возвращаемым функцией сокращения, является a b, то есть выход первой функции параметра сокращения является выходом функции сокращения. Зная, что означает сокращение, смеем утверждать, что приведенные выше рассуждения о сигнатуре типа верны.

3.4.2 Параметричность

Как только вы вводите переменную типа, появляется странная функция, называемая состоянием параметра.

Эта функция указывает, что функция будет действовать на все типы с одинаковым поведением.

// head :: [a] -> a

Голова для функции в качестве примера видно, что принимает [a] Возвращает a. Мы знаем, что кроме параметра есть массив, другой ничего не знает, поэтому ограничивается производительностью функции, работающей с массивом.

Что он может сделать с а, если он ничего не знает о а?

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

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

Давайте посмотрим на другой пример:

// reverse :: [a] -> [a]

Какова возможная цель реверса только из сигнатуры типа?

Опять же, он не может сделать ничего конкретного с файлом . Он не может превратить а в другой тип или ввести b; ни то, ни другое невозможно.

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

i-don't-think-so

Можно ли его переставить? Я думаю, что это нормально, но он должен достичь своей цели предсказуемым образом. Кроме того, также возможно удалить или повторить элемент.

i-think-it-is-ok

Дело в том, что в обоих случаях полиморфизм типа а резко сужает возможное поведение обратной функции.

hoogle

Это «сужение возможностей» позволяет нам воспользоваться чем-то вродеHoogleТакая поисковая система подписи типа для поиска нужной нам функции. Объем информации, который может содержать сигнатура типа, действительно огромен.

3.4.3 Бесплатные теоремы

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

// head :: [a] -> a
compose(f, head) === compose(head, map(f))

// filter :: (a -> Bool) -> [a] -> [a]
// 其中 f 和 p 是谓词函数
compose(map(f), filter(compose(p, f))) ===
  compose(filter(p), map(f))

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

В первом примере левая часть уравнения говорит, сначала нужно получить голову массива (Примечание переводчика: то есть первый элемент), а затем вызвать на ней функцию f; правая часть уравнения говорит, что сначала Вызовите f для каждого элемента, а затем возьмите заголовок возвращаемого результата. Два выражения эквивалентны, но первое намного быстрее.

Второй пример фильтра делает то же самое. В левой части уравнения говорится: сначала объедините f и p, чтобы проверить, какие элементы нужно отфильтровать, а затем фактически вызовите f через map (не забывайте, что filter не изменяет элементы в массиве, что гарантирует, что a останется без изменений); Правая часть уравнения говорит о том, что f сначала вызывается с помощью map, а затем элементы фильтруются в соответствии с p. Эти двое также равны.

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

Это всего лишь два примера, но теоремы, которые они передают, являются общими и применимы ко всем сигнатурам полиморфных типов. В JavaScript вы можете использовать некоторые инструменты для объявления правил перезаписи или напрямую использовать функцию компоновки для определения правил перезаписи. Короче говоря, преимущества этого очевидны и легко доступны, а возможности безграничны.

3.4.4 Ограничения типа

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

// sort :: Ord a => [a] -> [a]

Левая часть толстой стрелки указывает на то, что a должен быть объектом Ord или должен реализовывать интерфейс Ord.

Что такое ОРД? Откуда это? В языке строгого типа это может быть настраиваемый интерфейс, допускающий различные значения. Таким образом мы можем не только получить больше информации об A, не понимая, что должна делать функция Sort, но и ограничить круг функций. Мы называем этот интерфейс ограничениями типа.

// assertEqual :: (Eq a, Show a) => a -> a -> Assertion

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

3.4.5 Роль сигнатур типов

Подводя итог, роль сигнатуры типа такова:

  • Объявить ввод и вывод функции
  • Держите функции общими и абстрактными
  • Может использоваться для проверки времени компиляции
  • Лучшая документация для вашего кода

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

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

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