Рубрики
Без рубрики

Функциональные шаблоны программирования: поваренная книга

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

Автор оригинала: FreeCodeCamp Community Member.

Эта статья ориентирована с аудиторией, выпускной из функциональных библиотек, таких как Рамда Использование алгебраических типов данных. Мы используем отличные Crocks Библиотека для наших объявлений и помощников, хотя эти концепции могут обращаться к другим. Мы будем сосредоточены на демонстрации практических приложений и паттернов, не углубляясь в много теории.

Безопасно выполнять опасные функции

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

// darken :: Number -> String -> String
darken(0.1)("gray")
//=> "#343434"

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

darken(0.1)(null)
=> // Error: Passed an incorrect argument to a color function, please pass a string representation of a color.

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

import { darken } from "polished"
import { tryCatch, compose, either, constant, identity, curry } from "crocks"

// safeDarken :: Number -> String -> String
const safeDarken = curry(n =>
  compose(
    either(constant("inherit"), identity),
    tryCatch(darken(n))
  )
)

Trucatch Выполняет предоставленную функцию в блоке Try-Catch и возвращает тип суммы, называемого Результат Отказ По его сути, тип суммы в основном является «или» тип. Это означает, что Результат может быть либо Хорошо Если операция успешна или Ошибка . в случае сбоев. Другие примеры типов сумм включают Может быть , Либо , Async и так далее. либо Бесплатный помощник Point нарушает значение из Результат коробка и возвращает CSS по умолчанию наследство Если бы все пошло на юг или затемненный цвет, если все прошло хорошо.

safeDarken(0.5)(null)
//=> inherit

safeDarken(0.25)('green')
//=> '#004d00'

Принудительные типы, используя, возможно, помощники

С JavaScript мы часто бегаем в случаях, когда наши функции взорваются, потому что мы ожидаем конкретного типа данных, но вместо этого получаем другой. Crocks предоставляет безопасный , Посоветочная и SafeLift Функции, которые позволяют нам выполнить код более предсказуемо, используя Может быть тип. Давайте посмотрим на способ преобразования CAMELCASED Строки в заголовок.

import { safeAfter, safeLift, isArray, isString, map, compose, option } from "crocks"

// match :: Regex -> String -> Maybe [String]
const match = regex => safeAfter(isArray, str => str.match(regex))

// join :: String -> [String] -> String
const join = separator => array => array.join(separator)

// upperFirst :: String -> String
const upperFirst = x =>
  x.charAt(0)
    .toUpperCase()
    .concat(x.slice(1).toLowerCase())

// uncamelize :: String -> Maybe String
const uncamelize = safeLift(isString, compose(
  option(""),
  map(compose(join(" "), map(upperFirst))),
  match(/(((^[a-z]|[A-Z])[a-z]*)|[0-9]+)/g),
))

uncamelize("rockTheCamel")
//=> Just "Rock The Camel"

uncamelize({})
//=> Nothing

Мы создали функцию помощника Матч что использует Посоветочная утюгнуть String.prototype.match поведение возврата undefined В случае, если нет матчей. ИСАРАЙ предикат гарантирует, что мы получим Ничего Если нет найденных совпадений, а A Просто [строка] в случае матчей. Посоветочная отлично подходит для выполнения существующих или сторонних функций надежным безопасным способом.

(Совет: Buffter очень хорошо работает с Ramda Функции, которые возвращают A | undefined .)

Наше безвременно? Функция выполняется с SafeLift (IsString) Это означает, что он только будет выполнен, когда вход возвращает true для isString предикат.

В дополнение к этому, Crocks также предоставляет опора и Progpath помощники, которые позволяют выбрать недвижимость от Объект S и Массив с.

import { prop, propPath, map, compose } from "crocks"

const goodObject = {
  name: "Bob",
  bankBalance: 7999,
  address: {
    city: "Auckland",
    country: "New Zealand",
  },
}

prop("name")(goodObject)
//=> Just "Bob"
propPath(["address", "city"])(goodObject)
//=> Just "Auckland"

// getBankBalance :: Object -> Maybe String
const getBankBalance = compose(
  map(balance => balance.toFixed(2)),
  prop("bankBalance")
)

getBankBalance(goodObject)
//=> Just '7999.00'
getBankBalance({})
//=> Nothing

Это отлично, особенно если мы имеем дело с данными из побочных эффектов, которые не находятся под нашим контролем, как ответы API. Но что произойдет, если разработчики API внезапно решили обрабатывать форматирование в их конце?

const badObject = { 
  name: "Rambo",
  bankBalance: "100.00",
  address: {
    city: "Hope",
    country: "USA"
  }
}

getBankBalance(badObject) // TypeError: balance.toFixed is not a function :-(

Ошибки времени выполнения! Мы пытались вызвать Тофикс Способ на строке, который на самом деле не существует. Нам нужно убедиться, что Банкбаланс действительно является Номер прежде чем мы вызовем Тофикс в теме. Давайте попробуем решить это с нашими безопасный помощник.

import { prop, propPath, compose, map, chain, safe, isNumber } from "crocks"

// getBankBalance :: Object -> Maybe String
const getBankBalance = compose(
  map(balance => balance.toFixed(2)),
  chain(safe(isNumber)),
  prop("bankBalance")
)

getBankBalance(badObject) //=> Nothing
getBankBalance(goodObject) //=> Just '7999.00'

Мы проводят результаты опора Функция для нашего Безопасный (Isnumber) Функция, которая также возвращает Может быть в зависимости от того, будь результат опора удовлетворяет предикату. Трубопровод выше гарантирует, что последний карта который содержит Тофикс будет называться только когда Банкбаланс это Номер Отказ

Если вы собираетесь иметь дело с большим количеством подобных случаев, это имеет смысл извлечь этот шаблон в качестве помощника:

import { Maybe, ifElse, prop, chain, curry, compose, isNumber } from "crocks"

const { of, zero } = Maybe

// propIf :: (a -> Boolean) -> [String | Number] -> Maybe a
const propIf = curry((fn, path) =>
  compose(
    chain(ifElse(fn, of, zero)),
    prop(path)
  )
)

propIf(isNumber, "age", goodObject) 
//=> Just 7999
propIf(isNumber, "age", badObject) 
//=> Nothing

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

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

import { Maybe, safe, isNumber } from "crocks"

// safeNumber :: a -> Maybe a
const safeNumber = safe(isNumber)

// add :: a -> b -> Maybe Number
const add = (a, b) => {
  const maybeA = safeNumber(a)
  const maybeB = safeNumber(b)
  
  return maybeA.chain(
    valA => maybeB.map(valB => valA + valB)
  )
}

add(1, 2)
//=> Just 3

add(1, {})
//=> Nothing

Это делает именно то, что нам нужно, но наш Добавить Функция больше не просто A + B Отказ Это должно сначала поднять наши ценности в Может быть S, затем достичь их для доступа к значениям, а затем вернуть результат. Нам нужно найти способ сохранить основные функциональные возможности нашего Добавить Функция, позволяющая ему работать со значениями, содержащимися в объявлении! Вот где пригодны прикладные функторы.

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

of :: Applicative f => a -> f a

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

Maybe.of(null)
//=> Just null

Const.of(42)
//=> Const 42

И вот где все деньги – это AP Метод:

ap :: Apply f => f a ~> f (a -> b) -> f b

Подпись выглядит очень похоже на карта , с единственной разницей, чем наше A -> B Функция также завернута в F Отказ Давайте посмотрим на это в действии.

import { Maybe, safe, isNumber } from "crocks"

// safeNumber :: a -> Maybe a
const safeNumber = safe(isNumber)

// add :: a -> b -> c
const add = a => b => a + b 

// add :: a -> b -> Maybe Number
const safeAdd = (a, b) => Maybe.of(add)
  .ap(safeNumber(a))
  .ap(safeNumber(b))

safeAdd(1, 2)
//=> Just 3

safeAdd(1, "danger")
//=> Nothing

Сначала мы поднимаем наши каррики Добавить функция в Может быть , а затем подать заявку Может быть, и Может быть, б к этому. Мы использовали карта Пока чтобы получить доступ к значению внутри контейнера и AP ничем не отличается Внутренне, это карта на SafEnumber (A) Для доступа к А и применяет его к Добавить Отказ Это приводит к Может быть который содержит частично примененное Добавить Отказ Мы повторяем тот же процесс с SafeNumber (B) выполнить наши Добавить Функция, в результате чего Просто результата, если оба А и B действительны или Ничего иначе.

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

liftA2(add)(Maybe(1))(Maybe(2))
//=> Just 3

Мы будем использовать этот хелпер широко в разделе Выражение параллелизма Отказ

Совет: так как мы заметили, что AP использует карта Для доступа к значениям мы можем проделать крутые вещи, такие как генерация декартового продукта, когда два списка.

import { List, Maybe, Pair, liftA2 } from "crocks"

const names = List(["Henry", "George", "Bono"])
const hobbies = List(["Music", "Football"])

List(name => hobby => Pair(name, hobby))
  .ap(names)
  .ap(hobbies)
// => List [ Pair( "Henry", "Music" ), Pair( "Henry", "Football" ), 
// Pair( "George", "Music" ), Pair( "George", "Football" ), 
// Pair( "Bono", "Music" ), Pair( "Bono", "Football" ) ]

Использование Async для предсказуемой обработки ошибок

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

Часто мы бегаем в случаях, когда мы хотим сделать вызовы API, которые зависят друг над другом. Здесь GetUser Конечная точка возвращает пользовательский объект из GitHub, а ответ содержит много встроенных URL для репозиториев, звезд, фаворитов и так далее. Мы увидим, как мы можем разработать этот случай с использованием Async Отказ

import { Async, prop, compose, chain,  safe, isString, maybeToAsync } from "crocks"

const { fromPromise } = Async

// userPromise :: String -> Promise User Error
const userPromise = user => fetch(`https://api.github.com/users/${user}`)
  .then(res => res.json())

// resourcePromise :: String -> Promise Resource Error
const resourcePromise = url => fetch(url)
  .then(res => res.json())

// getUser :: String -> Async User Error
const getUser = compose(
  chain(fromPromise(userPromise)),
  maybeToAsync('getUser expects a string'),
  safe(isString)
)

// getResource :: String -> Object -> Async Resource Error
const getResource = path => user => {
  if (!isString(path)) {
    return Async.Rejected("getResource expects a string")
  }
  return maybeToAsync("Error: Malformed user response received", prop(path, user))
    .chain(fromPromise(resourcePromise))
}

// logError :: (...a) -> IO()
const logError = (...args) => console.log("Error: ", ...args)

// logResponse :: (...a) -> IO()
const logSuccess = (...args) => console.log("Success: ", ...args)

getUser("octocat")
  .chain(getResource("repos_url"))
  .fork(logError, logSuccess)
//=> Success: { ...response }

getUser(null)
  .chain(getResource("repos_url"))
  .fork(logError, logSuccess)
//=> Error: The user must be as string

getUser("octocat")
  .chain(getResource(null))
  .fork(logError, logSuccess)
//=> Error: getResource expects a string

getUser("octocat")
  .chain(getResource("unknown_path_here"))
  .fork(logError, logSuccess)
//=> Error: Malformed user response received

Использование MaybetoaAsync Преобразование позволяет нам использовать все функции безопасности, которые мы получаем от использования Может быть и принести их к нашему Async потоки. Теперь мы можем открыть ввод и другие ошибки как часть нашего Async потоки.

Используя моноиды эффективно

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

concat :: Monoid m => m a -> m a -> m a

Concat Позволяет нам объединить два моноида одного типа вместе с предварительно указанной операцией.

empty :: Monoid m => () => m a

Пустой Метод предоставляет нам элемент идентичности, что когда Concat с другими моноидами одного типа, вернет один и тот же элемент. Вот о чем я говорю.

import { Sum } from "crocks"

Sum.empty()
//=> Sum 0

Sum(10)
  .concat(Sum.empty())
//=> Sum 10

Sum(10)
  .concat(Sum(32))
//=> Sum 42

Само по себе это не выглядит очень полезным, но Crocks предоставляет несколько дополнительных моноидов вместе с помощниками mconcat , измеритель , mconcatmap и MreduceMap Отказ

import { Sum, mconcat, mreduce, mconcatMap, mreduceMap } from "crocks"

const array = [1, 3, 5, 7, 9]

const inc = x => x + 1

mconcat(Sum, array)
//=> Sum 25

mreduce(Sum, array)
//=> 25

mconcatMap(Sum, inc, array)
//=> Sum 30

mreduceMap(Sum, inc, array)
//=> 30

mconcat и измеритель Методы возьмите моноид и список элементов для работы с и применяют Concat ко всем их элементам. Единственная разница между ними в том, что mconcat Возвращает экземпляр моноида во время измеритель Возвращает необработанное значение. mconcatmap и MreduceMap Помощники работают так же, за исключением того, что они принимают дополнительную функцию, которая используется для отображения на каждом элементе перед вызовом Concat Отказ

Давайте посмотрим на другой пример моноида из Crocks , Первый Моноид. При объединении, Первый всегда будет возвращать первое, не пустое значение.

import { First, Maybe } from "crocks"

First(Maybe.zero())
  .concat(First(Maybe.zero()))
  .concat(First(Maybe.of(5)))
//=> First (Just 5)

First(Maybe.of(5))
  .concat(First(Maybe.zero()))
  .concat(First(Maybe.of(10)))
//=> First (Just 5)

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

import { curry, First, mreduceMap, flip, prop, compose } from "crocks"

/** tryProps -> a -> [String] -> Object -> b */
const tryProps = flip(object => 
  mreduceMap(
    First, 
    flip(prop, object),
  )
)
 
const a = {
  x: 5,
  z: 10,
  m: 15,
  g: 12
}

tryProps(["a", "y", "b", "g"], a)
//=> Just 12

tryProps(["a", "b", "c"], a)
//=> Nothing

tryProps(["a", "z", "c"], a)
//=> Just 10

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

import { 
  applyTo, mreduceMap, isString, isEmpty, mreduce, First, not, isNumber, chain
  compose, safe, and, constant, Maybe, map, equals, ifElse, isBoolean, option,
} from "crocks";

// isDate :: a -> Boolean
const isDate = x => x instanceof Date;

// lte :: Number -> Number -> Boolean
const lte = x => y => y <= x;

// formatBoolean :: a -> Maybe String
const formatBoolean = compose(
  map(ifElse(equals(true), constant("Yes"), constant("No"))),
  safe(isBoolean)
);

// formatNumber :: a -> Maybe String
const formatNumber = compose(
  map(n => n.toFixed(2)),
  safe(isNumber)
);

// formatPercentage :: a -> Maybe String
const formatPercentage = compose(
  map(n => n + "%"),
  safe(and(isNumber, lte(100)))
);

// formatDate :: a -> Maybe String
const formatDate = compose(
  map(d => d.toISOString().slice(0, 10)),
  safe(isDate)
);

// formatString :: a -> Maybe String
const formatString = safe(isString)

// autoFormat :: a -> Maybe String
const autoFormat = value =>
  mreduceMap(First, applyTo(value), [
    formatBoolean,
    formatPercentage,
    formatNumber,
    formatDate,
    formatString
  ]);

autoFormat(true)
//=> Just "Yes"

autoFormat(10.02)
//=> Just "10%"

autoFormat(255)
//=> Just "255.00"

autoFormat(new Date())
//=> Just "2019-01-14"

autoFormat("YOLO!")
//=> Just "YOLO!"

autoFormat(null)
//=> Nothing

Выражая параллелизм в точке

Мы могли бы столкнуться с случаями, где хочется выполнять несколько операций на одном количестве данных и каким-то образом объединять результаты. Crocks Предоставляет нам два метода для достижения этого. Первый шаблон использует типы продуктов Пара и Кортеж Отказ Давайте посмотрим на небольшой пример, где у нас есть объект, который выглядит так:

{ ids: [11233, 12351, 16312], rejections: [11233] }

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

const getIds = (object) => object.ids.filter(x => object.rejections.includes(x))

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

import { prop, compose, equals, filter, fanout, merge, liftA2 } from "crocks"

/**
 * object :: Record
 * Record :: {
 *  ids: [Number]
 *  rejection: [Number]
 * }
 **/
const object = { ids: [11233, 12351, 16312], rejections: [11233] }

// excludes :: [a] -> [b] -> Boolean
const excludes = x => y => !x.includes(y)

// difference :: [a] -> [a] -> [a]
const difference = compose(filter, excludes)

// getIds :: Record -> Maybe [Number]
const getIds = compose(
  merge(liftA2(difference)),
  fanout(prop("rejections"), prop("ids"))
)

getIds(object)
//=> Just [ 12351, 16312 ]

getIds({ something: [], else: 5 })
//=> Nothing

Одним из основных преимуществ использования подхода PointFree является то, что он побуждает нас нарушать нашу логику на меньшие кусочки. Теперь у нас есть многоразовый помощник РазницаLifta2 , как видно ранее) что мы можем использовать для слияние Оба половины Пара все вместе.

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

import {
  mreduceMap, applyTo, option, identity, objOf, map,
  converge, compose, Assign, isString, constant
} from "crocks"
import propIf from "./propIf"

// normalize :: String -> [Object] -> Object
const normalize = mreduceMap(
  Assign,
  converge(
    applyTo,
    identity,
    compose(
      option(constant({})),
      map(objOf),
      propIf(isString, "id")
    )
  )
)

normalize([{ id: "1", name: "Kerninghan" }, { id: "2", name: "Stallman" }])
//=> { 1: { id: '1', name: 'Kerninghan' }, 2: { id: '2', name: 'Stallman' } }

normalize([{ id: null}, { id: "1", name: "Knuth" }, { totally: "unexpected" }])
//=> { 1: { id: '1', name: 'Knuth' } }

Использование траверса и последовательности, чтобы обеспечить данное здравомыслие

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

import { compose, safe, isArray, reduce, map } from "crocks"

// sum :: [Number] -> Number
const sum = reduce((a, b) => a + b, 0)

// length :: [a] -> Number
const length = x => x.length;

// totalLength :: [String] -> Maybe Number 
const totalLength = compose(
  map(sum),
  map(map(length)),
  safe(isArray)
)

const goodInput = ["is", "this", "the", "real", "life?"]
totalLength(goodInput)
//=> Just 18

const badInput = { message: "muhuhahhahahaha!"}
totalLength(badInput)
//=> Nothing

Отлично. Мы убедились, что наша функция всегда возвращает Ничего Если это не получает Массив Отказ Этого достаточно, хотя?

totalLength(["stairway", "to", undefined])
//=> TypeError: x is undefined

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

// safeLength :: a -> Maybe Number 
const safeLength = safeLift(isString, length)

Если мы используем Девелофикация вместо Длина Как наша функция отображения, мы получим бы [Может быть, номер] вместо [Номер] И мы не можем использовать наш сумма функция больше. Вот где последовательность приходит в удобное.

import { sequence, Maybe, Identity } from "crocks"

sequence(Maybe, Identity(Maybe.of(1)))
//=> Just Identity 1

sequence(Array, Identity([1,2,3]))
//=> [ Identity 1, Identity 2, Identity 3 ]

sequence(Maybe, [Maybe.of(4), Maybe.of(2)])
//=> Just [ 4, 2 ]

sequence(Maybe, [Maybe.of(4), Maybe.zero()])
//=> Nothing

последовательность Помогает поменять внутренний тип с внешним типом при выполнении определенного эффект Учитывая, что внутренний тип является примером. последовательность на Личность довольно тупой – это просто карта S За внутренним типом и возвращает содержимое, завернутое в Личность контейнер. Для Список и Массив , последовательность использует Уменьшить В списке, чтобы объединить его содержимое, используя AP и Concat Отказ Давайте посмотрим на это в действии в нашем рекакторе Только длина выполнение.

// totalLength :: [String] -> Maybe Number 
const totalLength = compose(
  map(sum),
  chain(sequence(Maybe)),
  map(map(safeLength)),
  safe(isArray)
)

const goodString = ["is", "this", "the", "real", "life?"]
totalLength(goodString)
//=> Just 18

totalLength(["stairway", "to", undefined])
//=> Nothing

Большой! Мы построили полностью пуленепробиваемый Только длина Отказ Этот шаблон отображения над чем-то из A -> M B а затем используете последовательность настолько распространены, что у нас есть другой помощник по имени Traverse который выполняет обе операции вместе. Давайте посмотрим, как мы можем использовать Traverse Вместо последовательности в приведенном выше примере.

// totalLengthT :: [String] -> Maybe Number 
const totalLengthT = compose(
  map(sum),
  chain(traverse(Maybe, safeLength)),
  safe(isArray)
)

Там! Это работает точно так же. Если мы думаем об этом, наше последовательность Оператор в основном Traverse , с личность как функция картирования.

Примечание. Поскольку мы не можем сделать вывод внутреннего типа, используя JavaScript, мы должны явно предоставить конструктор типа в качестве первого аргумента Traverse и последовательность Отказ

Легко видеть, как последовательность и Traverse неоценимы для проверки данных. Давайте попробуем создать универсальный валидатор, который принимает схему и проверяет входной объект. Мы будем использовать Результат Тип, который принимает полугруппу на левой стороне, которая позволяет нам собирать ошибки. Полугруппа похожа на моноид, и он определяет Concat Метод – но в отличие от моноида, он не требует наличия Пустой метод. Мы также представляем функцию трансформации MaybetOrosult Ниже, это поможет нам взаимодействовать между Может быть и Результат Отказ

import {
  Result, isString, map, merge, constant, bimap, flip, propOr, identity, 
  toPairs, safe, maybeToResult, traverse, and, isNumber, compose
} from "crocks"

// length :: [a] -> Int
const length = x => x.length

// gte :: Number -> a -> Result String a
const gte = x => y => y >= x

// lte :: Number -> a -> Result String a
const lte = x => y => y <= x

// isValidName :: a -> Result String a
const isValidName = compose(
  maybeToResult("expected a string less than 20 characters"),
  safe(and(compose(lte(20), length), isString))
)

// isAdult :: a -> Result String a
const isAdult = compose(
  maybeToResult("expected a value greater than 18"),
  safe(and(isNumber, gte(18)))
)

/**
 *  schema :: Schema
 *  Schema :: {
 *    [string]: a -> Result String a
 *  }
 * */
const schema = {
  name: isValidName,
  age: isAdult,
}

// makeValidator :: Schema -> Object -> Result [String] Object
const makeValidator = flip(object =>
  compose(
    map(constant(object)),
    traverse(Result, merge((key, validator) =>
        compose(
          bimap(error => [`${key}: ${error}`], identity),
          validator,
          propOr(undefined, key)
        )(object)
      )
    ),
    toPairs
  )
)

// validate :: Object -> Result [String] Object
const validate = makeValidator(schema)

validate(({
  name: "Car",
  age: 21,
}))
//=> Ok { name: "Car", age: 21 }

validate(({
  name: 7,
  age: "Old",
}))
//=>  Err [ "name: expected a string less than 20 characters", "age: expected a value greater than 18" ]

Так как мы перевернули Макиядитель Функция, чтобы сделать больше подходящей для карри, нашего составить Цепь получает схему, которую мы должны подтвердить против первого. Сначала мы нарушаем схему в ключевое значение Пара S и пропустите значение каждого свойства до соответствующей функции проверки. В случае неудачи функции, мы используем Bimap Чтобы отобразить ошибку, добавьте еще несколько информации и верните ее как синглтон Массив Отказ Traverse будет тогда Concat Все ошибки, если они существуют или верните исходный объект, если это действительно. Мы могли бы также вернуть Строка вместо Массив , но Массив чувствует гораздо приятнее.

Благодаря Ян Хофманн-Хикс, Sinisa Louc. а также Дейл Фрэнсис Для их входов на этом посте.