Эти рецепты будут наиболее полезны для тех, кто переходит от функциональных библиотек, таких как ramda
, к использованию алгебраических типов данных (ADT). Мы будем использовать замечательную библиотеку crocks
для ADT и хелперов, хотя вы сможете применять эти концепции и с другими библиотеками. Я сделал акцент на демонстрации практических примеров и шаблонов, не углубляясь в теорию.
Безопасное выполнение опасных функций
Допустим нам нужно использовать функцию из сторонней библиотеки, назовём её darken
. Эта функция принимает множитель и значение, определяющее цвет, а возвращает затемнённый оттенок этого цвета.
// darken :: Number -> String -> String
darken(0.1)("gray")
//=> "#343434"
Полезная штука для использования в CSS. Но оказывается, что функция darken
не так безобидна, как может показаться. darken
выдаёт ошибки, получая непредусмотренные аргументы.
darken(0.1)(null)
=> // Error: Passed an incorrect argument to a color function, please pass a string representation of a color.
С одной стороны, это полезно для отладки, но мы не хотим, чтобы приложение вылетало, если мы просто не задали цвет. Здесь нам поможет tryCatch
.
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))
)
)
tryCatch
выполняет нашу функцию в блоке try-catch и возвращает Sum Type с именем Result
. По своей сути, Sum Type является типом «or». Это означает, что Result
может принять значение либо Ok
, если операция выполнена успешно, либо Error
в случае ошибки. Другие варианты Sum Types могут быть: Maybe
, Either
, Async
и тд. Например, point-free хелпер either
берёт значение из Result
; если что-то пошло не так, то возвращается дефолтное CSS inherit
, а если все прошло хорошо, то мы получаем нужный нам оттенок.
safeDarken(0.5)(null)
//=> inherit
safeDarken(0.25)('green')
//=> '#004d00'
Используем Maybe хелперы для усиления типов
В JavaScript часто бывает, что в функцию приходит не тот тип данных, который мы ожидали, из-за чего мы получаем непредсказуемый результат. Библиотека crocks
предлагает нам функции safe
, safeAfter
и safeLift
, которые позволяют выполнять код более предсказуемо, благодаря типу Maybe
. Рассмотрим пример, как конвертировать camelCased строку в Title Case.
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
Мы создали вспомогательную функцию match
, которая задействует safeAfter
, чтобы «сгладить» поведение String.prototype.match
, когда нужно вернуть undefined
в случае отсутствия совпадений. Предикат isArray
проверяет наличие совпадений, если совпадений не найдено — мы получим Nothing
, в обратном случае Just [String]
. safeAfter
отлично служит для выполнения существующих или сторонних функций безопасным способом.
Совет: safeAfter
отлично работает с функцией ramda
, которая возвращает a | undefined
.
Наша функция uncamelize ?
выполняется с safeLift(isString);
это означает что она будет выполнена, только когда предикат isString
вернёт true.
Кроме того, crocks позволяет использовать хелперы prop
и propPath
, чтобы брать свойства из объектов и массивов.
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 :-(
Ошибки выполнения. Мы пытались вызвать метод toFixed
для несуществующей строки. Нам нужно убедится, что bankBalance
действительно число, перед тем как вызывать toFixed
. Давайте решим это с помощью хелпера safe
.
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'
Мы обеспечили доступность результатов функции prop
для функции safe(isNumber)
, которая, кроме того, возвращает Maybe
, если результат prop
удовлетворяет условиям предиката. Такой пайплайн гарантирует, что последний map
, который содержит toFixed
будет вызван только если bankBalance
является числом.
Чтобы применять его в похожих задачах, имеет смысл сделать из этого паттерна хелпер:
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
Чтобы код был чистым — используем аппликативы
Часто бывает так, что нам нужно использовать существующую функцию со значениями, которые упакованы в контейнер. Давайте создадим безопасную функцию add
, которая допускает только числа. Используем туже идею, что и в предыдущем примере. Первая попытка:
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
Код делает в точности то, что нам нужно, но функция add
теперь сложнее, чем просто a + b
. Сначала мы должны поднять значения в Maybe
, затем нужно получить к ним доступ и в итоге вернуть результат. Нам нужно найти способ сохранить основную функциональность функции add
, при этом обеспечить ей возможность работать со значениями, которые содержатся в ADT. В этом нам помогут аппликативные функторы.
Аппликативные функторы похожи на обычные, но помимо map
, они также реализуют два дополнительных метода:
of :: Applicative f => a -> f a
of
— простой конструктор, который поднимает любое значение, которое вы ему передадите, к нужному типу данных. В других языках он известен, как pure
.
Maybe.of(null)
//=> Just null
Const.of(42)
//=> Const 42
А самый интересный нам метод, это — ap
:
ap :: Apply f => f a ~> f (a -> b) -> f b
Очень похоже на map
, с одним отличием: функция 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
Сначала мы поднимаем функцию add
в Maybe
, затем применяем к ней Maybe a
и Maybe b
. До сих пор мы использовали map
для доступа к значению внутри контейнера и ap
. Другими словами мы получаем доступ к a
через safeNumber(a)
, и применяем его значение к add
. В результате получаем Maybe
, в котором содержится частично применённое значение add
. Повторяем тот же процесс для safeNumber(b)
, чтобы выполнить функцию add
. Итог: если a
и b
валидны, то результат будет Just
, иначе Nothing
.
В библиотека crocks
есть и другие хелперы: liftA2
и liftN
, они позволяют выразить туже концепцию, но в стиле pointfree. Типичный пример:
liftA2(add)(Maybe(1))(Maybe(2))
//=> Just 3
Мы будем широко использовать этот хелпер в разделе Expressing Parallelism
.
Подсказка: вы уже знаете, что ap
использует map
для доступа к значениям, таким образом мы можем делать классные вещи, например, сгенерировать “Декартово произведение” из двух списков:
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
возвращает user entity из 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
Используя трансформацию maybeToAsync
, мы можем перенести все фичи безопасности из Maybe
в Async
. Мы можем помечать входящие и другие ошибки, как часть потока Async
.
Эффективное использование Моноидов
Мы уже использовали моноиды в JavaScript, когда выполняли операции String
/Array
конкатенацию и добавление чисел. Это просто тип данных, который предлагает нам использовать следующие методы:
concat :: Monoid m => m a -> m a -> m a
concat
позволяет объединить два моноида одного типа вместе, заданным способом.
empty :: Monoid m => () => m a
У метода empty
есть элемент identity, который возвращает тот же элемент, после конкатенации с другими моноидами того же типа:
import { Sum } from "crocks"
Sum.empty()
//=> Sum 0
Sum(10)
.concat(Sum.empty())
//=> Sum 10
Sum(10)
.concat(Sum(32))
//=> Sum 42
Сам по себе этот код не выглядит особо полезным, но в библиотеке crocks
есть и другие моноиды с хелперами: mconcat
, mreduce
, 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
и mreduce
применяют concat
ко всем элементам из списка, для этого нужен моноид и сам список элементов. Методы отличаются тем, что mconcat
возвращает экземпляр моноида, а mreduce
возвращает сырое значение. Хелперы mconcatMap
и mreduceMap
работают схожим образом, за исключением того, что они принимают дополнительную функцию, которая служит для сопоставления каждого элемента перед вызовом concat
.
Давайте разберём моноид First
из библиотеки crocks
. При конкатенации, First
всегда возвращает первое непустое значение.
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)
Давайте создадим функцию, которая будет доставать первое доступное свойство объекта, используя для этого возможности First
.
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
Параллелизм в Pointfree стиле
Иногда нам нужно совершать множество операций с одним фрагментом данных, после чего объединять результаты этих операций. В библиотеке crocks
, для этого есть два метода. Первый шаблон использует Product типы Pair
и Tuple
. Давайте рассмотрим пример, в котором мы будем работать с таким объектом:
{ ids: [11233, 12351, 16312], rejections: [11233] }
Нам нужно написать функцию, которая принимает этот объект и возвращает массив из ids
, за исключением отклонённых элементов. Для начала можно попробовать так (JavaScript):
const getIds = (object) => object.ids.filter(x => object.rejections.includes(x))
Этот код, конечно, работает, но до тех пор, пока свойства корректны и определены. Вместо этого сделаем так, чтобы getIds
возвращал Maybe
. Мы используем хелпер fanout
, который принимает две функции, запускает их на одном входе и возвращает два результата.
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» подхода в том, что он подталкивает нас разбивать логику на маленькие части. Теперь у нас есть «многоразовый» хелпер difference
(с liftA2
, как мы видели ранее), который можно использовать для объединения результата.
Во втором методе будем использовать комбинатор converge
, для достижения аналогичных результатов. converge
принимает три функции и входящее значение, затем применяет это значение ко второй и третьей функции, а их результаты направляет в первую. Используем это, чтобы создать функцию, которая нормализует массив объектов на основе их id
. Мы применим моноид Assign
, который позволит объединить объекты вместе.
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' } }
Обеспечение сохранности данных с помощью traverse
и sequence
Мы видели, как работает Maybe
и его «друзья» чтобы предоставить нам те типы, на которые мы рассчитываем. Но что происходит, когда мы работаем с типом, который содержит другие значения, например массив или список? Давайте рассмотрим простую функцию, которая возвращает общую длину всех строк, содержащихся в массиве.
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
Отлично. Мы убедились, что наша функция всегда возвращает Nothing
, если она не получает массив. Этого достаточно?
totalLength(["stairway", "to", undefined])
//=> TypeError: x is undefined
Не совсем. Эта функция не гарантирует, что список не содержит неприятных сюрпризов. Один из способов решить эту проблему, это определить функцию safeLength
, которая работает только со строками:
// safeLength :: a -> Maybe Number
const safeLength = safeLift(isString, length)
Если в качестве mapping функции мы используем safeLength
, вместо length
, то получим [Maybe Number]
вместо [Number]
и больше не сможем использовать функцию sum
. Здесь нам поможет sequence
.
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
sequence
помогает поменять местами внутренний тип и внешний во время выполнения определенного эффекта, учитывая, что внутренний тип является аппликативным. Использование sequence
с Identity
устанавливает соответствие внутреннего типа и возвращает содержимое, упакованное в контейнер Identity
. Для списка и массива, sequence
использует reduce
, чтобы объединить содержимое с помощью ap
и concat
. Давайте посмотрим, как это работает на примере переработанной реализации totalLength
.
// 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
Отлично. Теперь totalLength
абсолютно надёжна. Такой шаблон маппинга (от a -> m b
затем использование sequence
) настолько распространён, что у нас появился ещё один хелпер, под названием traverse
, который выполняет обе операции вместе. Давайте посмотрим, как можно применять traverse
вместо sequence
в нашем примере.
// totalLengthT :: [String] -> Maybe Number
const totalLengthT = compose(
map(sum),
chain(traverse(Maybe, safeLength)),
safe(isArray)
)
Ну вот, работает точно также. Получается, что оператор sequence
с identity
в качестве маппинг функции, по сути, делает тоже самое, что и traverse.
Примечание: поскольку мы не можем вывести внутренний тип с помощью JavaScript, мы должны явно предоставить конструктор типа в качестве первого аргумента для traverse
и sequence
.
Становится очевидно насколько sequence
и traverse
полезны для валидации данных. Давайте создадим универсальный валидатор, который принимает схему и проверяет входной объект. Для этого используем тип Result
, который принимает полугруппу с левой стороны, для сбора ошибок. Полугруппа похожа на моноид, она определяет метод concat
, но в отличии от моноида не требует наличия метода empty
. В коде присутствует функция maybeToResult
, она поможет нам взаимодействовать между Maybe
и Result
.
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" ]
Поскольку мы перевернули функцию makeValidator
, цепочка compose
получает схему, которую нам нужно проверить. Сначала мы разбиваем схему на пары «ключ-значение», затем передаём значение каждого свойства соответствующей функции валидации. На случай если в функции произойдёт ошибка, мы применили bimap
для map, добавили дополнительную информацию и возвращаем как синглтон массив. Затем traverse
сделает конкатенацию всех ошибок, если они были или вернёт исходный объект, если он прошёл валидацию. Ещё мы можем вернуть строку вместо массива, но с массивом как-то удобней.
Перевод статьи Karthik Iyengar: Functional Programming Patterns: A Cookbook