Acest articol vizează un public care absolveste biblioteci funcționale precum ramda la utilizarea tipurilor de date algebrice. Folosim excelentul crocks bibliotecă pentru ADT-urile și asistenții noștri, deși aceste concepte se pot aplica și celorlalte. Ne vom concentra pe demonstrarea aplicațiilor practice și a modelelor fără a ne adânci în multă teorie.

Executarea în siguranță a funcțiilor periculoase

Să presupunem că avem o situație în care vrem să folosim o funcție numită darken dintr-o bibliotecă terță parte. darken ia un multiplicator, o culoare și returnează o nuanță mai închisă a acelei culori.

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

Destul de la îndemână pentru nevoile noastre de CSS. Dar se pare că funcția nu este atât de inocentă pe cât pare. darken aruncă erori când primește argumente neașteptate!

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

Acest lucru este, desigur, foarte util pentru depanare – dar nu am vrea ca aplicația noastră să explodeze doar pentru că nu am putut obține o culoare. Iată unde tryCatch vine în ajutor.

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 execută funcția furnizată într-un bloc try-catch și returnează un tip de sumă numit Result. În esența sa, un tip de sumă este practic un tip „sau”. Aceasta înseamnă că Result ar putea fi fie un Ok dacă o operațiune are succes sau un Error în caz de eșecuri. Alte exemple de tipuri de sume includ Maybe, Either, Async si asa mai departe. either asistent fără puncte rupe valoarea din Result și returnează valoarea implicită CSS inherit dacă lucrurile mergeau spre sud sau culoarea întunecată dacă totul mergea bine.

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

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

Aplicarea tipurilor folosind Helpers poate

Cu JavaScript, întâlnim adesea cazuri în care funcțiile noastre explodează deoarece ne așteptăm la un anumit tip de date, dar primim altul în schimb. crocks oferă safe, safeAfter și safeLift funcții care ne permit să executăm cod mai previzibil prin utilizarea Maybe tip. Să ne uităm la o modalitate de a converti șirurile de caractere camelCased în Title Case.

ad-banner
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

Am creat o funcție de ajutor match care folosește safeAfter a calca String.prototype.matchComportamentul de a returna un undefined în cazul în care nu există meciuri. isArray predicat ne asigură că primim un Nothing dacă nu sunt găsite potriviri și a Just [String] în cazul meciurilor. safeAfter este excelent pentru executarea funcțiilor existente sau ale terților într-un mod sigur și sigur.

(Bacsis: safeAfter funcționează foarte bine cu ramda funcții care revin a | undefined.)

Al nostru uncamelize ? funcția este executată cu safeLift(isString) ceea ce înseamnă că se va executa numai când intrarea revine adevărată pentru isStringpredicat.

În plus față de aceasta, cocoșele oferă și prop și propPath ajutoare care vă permit să alegeți proprietăți de la Objects și Arrays.

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

Acest lucru este extraordinar, mai ales dacă avem de-a face cu date de la efecte secundare care nu sunt sub controlul nostru, cum ar fi răspunsurile API. Dar ce se întâmplă dacă dezvoltatorii API decid brusc să gestioneze formatarea la sfârșitul lor?

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

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

Erori în timpul rulării! Am încercat să invocăm toFixed metoda pe un șir, care nu există cu adevărat. Trebuie să ne asigurăm că bankBalance este într-adevăr un Number înainte de a invoca toFixed pe el. Să încercăm să o rezolvăm cu a noastră safe ajutor.

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'

Rezultăm rezultatele prop funcția noastră safe(isNumber) funcție care returnează și a Maybe, în funcție de rezultatul propsatisface predicatul. Conducta de mai sus garantează că ultima mapcare conține toFixed va fi apelat doar când bankBalance este o Number.

Dacă aveți de-a face cu o mulțime de cazuri similare, ar avea sens să extrageți acest model ca ajutor:

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

Utilizarea aplicațiilor pentru a menține funcțiile curate

De multe ori, ne găsim în situații în care am vrea să folosim o funcție existentă cu valori înfășurate într-un container. Să încercăm să proiectăm un seif add funcție care permite numai numere, utilizând conceptele din secțiunea anterioară. Iată prima noastră încercare.

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

Aceasta face exact ceea ce avem nevoie, dar al nostru add funcția nu mai este o simplă a + b. Mai întâi trebuie să ne ridice valorile Maybes, apoi accesați-le pentru a accesa valorile și apoi întoarceți rezultatul. Trebuie să găsim o modalitate de a păstra funcționalitatea de bază a noastră add funcțional, permițându-i să funcționeze cu valorile conținute în ADT-uri! Iată unde funcționalele aplicative sunt utile.

Un funcțional aplicativ este doar un funcționor obișnuit, dar împreună cu map, implementează și două metode suplimentare:

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

of este un constructor complet prost și ridică orice valoare pe care o acordați în tipul nostru de date. Se mai numește și pure în alte limbi.

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

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

Și aici sunt toți banii – ap metodă:

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

Semnătura arată foarte asemănătoare cu map, singura diferență fiind aceea a noastră a -> b funcția este, de asemenea, înfășurată într-un fișier f. Să vedem acest lucru în acțiune.

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

Mai întâi ne ridicăm curry-ul add funcționează într-un Maybe, apoi aplicați Maybe ași Maybe b la ea. Am folosit map până acum pentru a accesa valoarea din interiorul unui container și ap nu este diferit. Pe plan intern mapeste pe safeNumber(a) pentru a accesa a și o aplică la add. Acest lucru are ca rezultat un Maybe care conține un parțial aplicat add. Repetăm ​​același proces cu safeNumber(b) pentru a ne executa add funcție, rezultând o Just a rezultatului dacă ambele a și bsunt valabile sau a Nothing in caz contrar.

Crocks ne oferă, de asemenea liftA2 și liftN ajutoarele să exprime același concept într-un mod lipsit de puncte. Urmează un exemplu banal:

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

Vom folosi acest ajutor în secțiune Expressing Parallelism.

Sfat: Deoarece am observat asta ap utilizări map pentru a accesa valori, putem face lucruri interesante, cum ar fi generarea unui produs cartezian, când ni se oferă două liste.

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" ) ]

Utilizarea Async pentru gestionarea predictibilă a erorilor

crocks oferă Async tip de date care ne permite să construim calcule asincrone leneșe. Pentru a afla mai multe despre aceasta, puteți consulta documentația oficială extinsă aici. Această secțiune își propune să ofere exemple despre modul în care putem folosi Async pentru a îmbunătăți calitatea raportării erorilor și pentru a face codul nostru rezistent.

Adesea, întâlnim cazuri în care dorim să facem apeluri API care depind unul de celălalt. Aici getUser endpoint returnează o entitate utilizator din GitHub și răspunsul conține o mulțime de adrese URL încorporate pentru depozite, stele, favorite și așa mai departe. Vom vedea cum putem proiecta acest caz de utilizare cu utilizarea 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

Utilizarea maybeToAsync transformarea ne permite să folosim toate caracteristicile de siguranță pe care le obținem din utilizarea Maybe și adu-i la noi Asynccurge. Acum putem semnaliza erorile de intrare și alte erori ca parte a noastră Async curge.

Utilizarea eficientă a monoizilor

Folosim deja Monoide când efectuăm operațiuni de genul String/Array concatenare și adăugare de număr în JavaScript nativ. Este pur și simplu un tip de date care ne oferă următoarele metode.

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

concat ne permite să combinăm doi monoizi de același tip împreună cu o operație pre-specificată.

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

empty metoda ne oferă un element de identitate, atunci când concat editat cu alți monoizi de același tip, ar returna același element. Iată despre ce vorbesc.

import { Sum } from "crocks"

Sum.empty()
//=> Sum 0

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

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

În sine, acest lucru nu pare foarte util, dar crocks oferă câteva Monoide suplimentare împreună cu ajutoare mconcat, mreduce, mconcatMap și 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 și mreduce metodele iau un Monoid și o listă de elemente pentru a lucra și se aplică concat la toate elementele lor. Singura diferență dintre ele este că mconcat returnează o instanță a Monoidului în timp ce mreduce returnează valoarea brută. mconcatMap și mreduceMap asistenții funcționează în același mod, cu excepția faptului că acceptă o funcție suplimentară care este utilizată pentru maparea fiecărui element înainte de a apela concat.

Să ne uităm la un alt exemplu de Monoid din crocks, First Monoid. La concatenare, First va returna întotdeauna prima valoare, care nu este goală.

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)

Folosind puterea lui First, să încercăm să creăm o funcție care încearcă să obțină prima proprietate disponibilă pe un obiect.

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

Destul de curat! Iată un alt exemplu care încearcă să creeze un formatator cu cel mai bun efort atunci când sunt furnizate diferite tipuri de valori.


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

Exprimarea paralelismului într-o manieră fără puncte

S-ar putea să întâlnim cazuri în care dorim să efectuăm mai multe operații pe o singură bucată de date și să combinăm rezultatele într-un fel. crocks ne oferă două metode pentru a realiza acest lucru. Primul model utilizează tipurile de produse Pair și Tuple. Să vedem un mic exemplu în care avem un obiect care arată astfel:

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

Am dori să scriem o funcție care acceptă acest obiect și returnează un Array de ids excluzându-le pe cele respinse. Prima noastră încercare în JavaScript nativ ar arăta astfel:

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

Desigur, acest lucru funcționează, dar ar exploda în cazul în care una dintre proprietăți este malformată sau nu este definită. Să facem getIds retur a Maybe in schimb. Folosim fanout helper care acceptă două funcții, îl rulează pe aceeași intrare și returnează un Pair a rezultatelor.

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

Unul dintre principalele beneficii ale utilizării abordării fără puncte este că ne încurajează să ne rupem logica în bucăți mai mici. Acum avem ajutorul reutilizabil difference (cu liftA2, așa cum am văzut anterior), pe care le putem folosi merge ambele jumătăți Pair împreună.

A doua metodă ar fi să utilizați converge combinator pentru a obține rezultate similare. converge preia trei funcții și o valoare de intrare. Apoi aplică intrarea la a doua și a treia funcție și canalizează rezultatele ambelor în prima. Să-l folosim pentru a crea o funcție care normalizează un Arrayde obiecte bazate pe lor ids. Vom folosi Assign Monoid care ne permite să combinăm obiecte împreună.

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' } }

Utilizarea Traverse și Secvență pentru a asigura sănătatea datelor

Am văzut cum să folosim Maybe și prieteni pentru a ne asigura că lucrăm întotdeauna cu tipurile pe care le așteptăm. Dar ce se întâmplă când lucrăm cu un tip care conține alte valori, cum ar fi un Array sau a List de exemplu? Să vedem o funcție simplă care ne oferă lungimea totală a tuturor șirurilor conținute într-un Array.

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

Grozav. Ne-am asigurat că funcția noastră revine întotdeauna a Nothing dacă nu primește un Array. Este suficient, totuși?

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

Nu chiar. Funcția noastră nu garantează că conținutul listei nu va avea surprize. Unul dintre modurile în care am putea rezolva acest lucru ar fi să definim un safeLength funcție care funcționează numai cu șiruri:

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

Dacă folosim safeLength in loc de length ca funcție de cartografiere, am primi un [Maybe Number] în loc de a [Number] și nu ne putem folosi de noi sumfuncționează mai mult. Iată unde sequence vine la îndemână.

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 ajută la schimbul tipului interior cu tipul exterior în timp ce efectuați un anumit effect, având în vedere că tipul interior este aplicativ. sequence pe Identity este destul de prost – este doar maps peste tipul interior și returnează conținutul înfășurat într-un Identity container. Pentru List și Array, sequenceutilizări reduce pe listă pentru a combina conținutul său folosind ap și concat. Să vedem acest lucru în acțiune în refactorizarea noastră totalLength implementare.

// 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

Grozav! Am construit un antiglonț complet totalLength. Acest model de mapare peste ceva din a -> m b și apoi folosind sequence este atât de comun încât avem un alt ajutor numit traverse care efectuează ambele operații împreună. Să vedem cum putem folosi traverse în loc de secvență în exemplul de mai sus.

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

Acolo! Funcționează exact la fel. Dacă ne gândim la asta, a noastră sequenceoperatorul este practic traverse, cu un identity ca funcție de cartografiere.

Notă: Deoarece nu putem deduce tipul interior folosind JavaScript, trebuie să furnizăm în mod explicit constructorul de tip ca prim argument traverse și sequence.

Este ușor de văzut cum sequence și traverse sunt de neprețuit pentru validarea datelor. Să încercăm să creăm un validator generic care ia o schemă și validează un obiect de intrare. Vom folosi Result type, care acceptă un semigrup în partea stângă care ne permite să colectăm erori. Un semigrup este similar cu un monoid și definește un concat metoda – dar spre deosebire de Monoid, nu necesită prezența empty metodă. Introducem și funcția de transformare maybeToResult mai jos, asta ne va ajuta să interoperăm între Maybe și 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" ]

De când am răsturnat makeValidator funcția de a face mai potrivit pentru currying, nostru compose chain primește schema pe care trebuie să o validăm mai întâi. Mai întâi împărțim schema în valoare-cheie Pairs și treceți valoarea fiecărei proprietăți funcției de validare corespunzătoare. În cazul în care funcția eșuează, vom folosi bimap pentru a mapa eroarea, adăugați-i mai multe informații și returnați-le sub formă de singleton Array. traverse va atunci concat toate erorile dacă există sau returnează obiectul original dacă este valid. Am fi putut întoarce și un String în loc de un Array, dar un Arrayse simte mult mai frumos.

Mulțumim lui Ian Hofmann-Hicks, Sinisa Louc și Dale Francis pentru contribuțiile lor la acest post.