Articles précédents de la série:
Dans l'article précédent, nous avons examiné le concept de classe de types et nous nous sommes briÚvement familiarisés avec les classes de types "fonctor", "monad", "monoid". Dans cet article, j'ai promis d'aborder l'idée des effets algébriques, mais j'ai décidé d'écrire sur le travail avec des types et des exceptions Nullables, afin que la discussion plus approfondie soit plus claire lorsque nous passerons au travail avec des tùches et des effets. Par conséquent, dans cet article, toujours destiné aux développeurs FP débutants, je veux parler d'une approche fonctionnelle pour résoudre certains des problÚmes d'application que vous devez faire face au quotidien.
Comme toujours, je vais illustrer des exemples utilisant des structures de données de la bibliothÚque fp-ts .
Il est déjà devenu un peu mauvais de citer Tony Hoare avec son "erreur en un milliard" - l'introduction du concept de pointeur nul vers le langage ALGOL W. Cette erreur, comme une tumeur, s'est propagée à d'autres langages - C, C ++, Java et, enfin, JS. La possibilité d'attribuer une valeur de n'importe quel type à une variable null
entraĂźne des effets secondaires indĂ©sirables lors de la tentative d'accĂšs par ce pointeur - le runtime lĂšve une exception, le code doit donc ĂȘtre recouvert de logique pour gĂ©rer de telles situations. Je pense que vous avez tous rencontrĂ© (ou mĂȘme Ă©crit) du code de type nouilles comme:
function foo(arg1, arg2, arg3) {
if (!arg1) {
return null;
}
if (!arg2) {
throw new Error("arg2 is required")
}
if (arg3 && arg3.length === 0) {
return null;
}
// - -, arg1, arg2, arg3
}
TypeScript â strictNullChecks
-nullable null
, TS2322. - , never
, . , API add :: (x: number, y: number) => number
, - , . , Java throws
, try-catch
, TypeScript -, () JSDoc-, .
, . , JVM-: Error () â , (, ); exception () â , (, ). JS/TS- , (throw new Error()
), . , â « , ».
â « » â .
Option<A>
â nullable-
JS TS nullable- optional chaining nullish coalescing. , , . , optional chaining â if (a != null) {}
, Go:
const getNumber = (): number | null => Math.random() > 0.5 ? 42 : null;
const add5 = (n: number): number => n + 5;
const format = (n: number): string => n.toFixed(2);
const app = (): string | null => {
const n = getNumber();
const nPlus5 = n != null ? add5(n) : null;
const formatted = nPlus5 != null ? format(nPlus5) : null;
return formatted;
};
Option<A>
, : None
, Some
A
:
type Option<A> = None | Some<A>;
interface None {
readonly _tag: 'None';
}
interface Some<A> {
readonly _tag: 'Some';
readonly value: A;
}
, , . «», null, , .
import { Monad1 } from 'fp-ts/Monad';
const URI = 'Option';
type URI = typeof URI;
declare module 'fp-ts/HKT' {
interface URItoKind<A> {
readonly [URI]: Option<A>;
}
}
const none: None = { _tag: 'None' };
const some = <A>(value: A) => ({ _tag: 'Some', value });
const Monad: Monad1<URI> = {
URI,
// :
map: <A, B>(optA: Option<A>, f: (a: A) => B): Option<B> => {
switch (optA._tag) {
case 'None': return none;
case 'Some': return some(f(optA.value));
}
},
// :
of: some,
ap: <A, B>(optAB: Option<(a: A) => B>, optA: Option<A>): Option<B> => {
switch (optAB._tag) {
case 'None': return none;
case 'Some': {
switch (optA._tag) {
case 'None': return none;
case 'Some': return some(optAB.value(optA.value));
}
}
}
},
// :
chain: <A, B>(optA: Option<A>, f: (a: A) => Option<B>): Option<B> => {
switch (optA._tag) {
case 'None': return none;
case 'Some': return f(optA.value);
}
}
};
, . â chain
( bind flatMap ) of
(pure return).
JS/TS , Haskell Scala, nullable-, , , â , (, , ) (Promise/A+, async/await, optional chaining). , - TC39, , .
Option fp-ts/Option
, , :
import { pipe, flow } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import Option = O.Option;
const getNumber = (): Option<number> => Math.random() > 0.5 ? O.some(42) : O.none;
// !
const add5 = (n: number): number => n + 5;
const format = (n: number): string => n.toFixed(2);
const app = (): Option<string> => pipe(
getNumber(),
O.map(n => add5(n)), // O.map(add5)
O.map(format)
);
, , app
:
const app = (): Option<string> => pipe(
getNumber(),
O.map(flow(add5, format)),
);
N.B. - ( ), : « -», Option ( ) - ( ). ///etc , -. â , Free- Tagless Final. , â .
Either<E, A>
â ,
. , â , - . â , Option, Either:
type Either<E, A> = Left<E> | Right<A>;
interface Left<E> {
readonly _tag: 'Left';
readonly left: E;
}
interface Right<A> {
readonly _tag: 'Right';
readonly right: A;
}
Either<E, A>
, : , E
, , A
. , , â . Either â ////etc, fp-ts/Either
. :
import { Monad2 } from 'fp-ts/Monad';
const URI = 'Either';
type URI = typeof URI;
declare module 'fp-ts/HKT' {
interface URItoKind2<E, A> {
readonly [URI]: Either<E, A>;
}
}
const left = <E, A>(e: E) => ({ _tag: 'Left', left: e });
const right = <E, A>(a: A) => ({ _tag: 'Right', right: a });
const Monad: Monad2<URI> = {
URI,
// :
map: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => B): Either<E, B> => {
switch (eitherEA._tag) {
case 'Left': return eitherEA;
case 'Right': return right(f(eitherEA.right));
}
},
// :
of: right,
ap: <E, A, B>(eitherEAB: Either<(a: A) => B>, eitherEA: Either<A>): Either<B> => {
switch (eitherEAB._tag) {
case 'Left': return eitherEAB;
case 'Right': {
switch (eitherEA._tag) {
case 'Left': return eitherEA;
case 'Right': return right(eitherEAB.right(eitherEA.right));
}
}
}
},
// :
chain: <E, A, B>(eitherEA: Either<E, A>, f: (a: A) => Either<E, B>): Either<E, B> => {
switch (eitherEA._tag) {
case 'Left': return eitherEA;
case 'Right': return f(eitherEA.right);
}
}
};
, , . , Either, . , API , email , :
- Email «@»;
- Email «@»;
- Email «@», 1 , 2 ;
- 1 .
, . , , :
interface Account {
readonly email: string;
readonly password: string;
}
class AtSignMissingError extends Error { }
class LocalPartMissingError extends Error { }
class ImproperDomainError extends Error { }
class EmptyPasswordError extends Error { }
type AppError =
| AtSignMissingError
| LocalPartMissingError
| ImproperDomainError
| EmptyPasswordError;
- :
const validateAtSign = (email: string): string => {
if (!email.includes('@')) {
throw new AtSignMissingError('Email must contain "@" sign');
}
return email;
};
const validateAddress = (email: string): string => {
if (email.split('@')[0]?.length === 0) {
throw new LocalPartMissingError('Email local-part must be present');
}
return email;
};
const validateDomain = (email: string): string => {
if (!/\w+\.\w{2,}/ui.test(email.split('@')[1])) {
throw new ImproperDomainError('Email domain must be in form "example.tld"');
}
return email;
};
const validatePassword = (pwd: string): string => {
if (pwd.length === 0) {
throw new EmptyPasswordError('Password must not be empty');
}
return pwd;
};
const handler = (email: string, pwd: string): Account => {
const validatedEmail = validateDomain(validateAddress(validateAtSign(email)));
const validatedPwd = validatePassword(pwd);
return {
email: validatedEmail,
password: validatedPwd,
};
};
, â API , . Either:
import * as E from 'fp-ts/Either';
import { pipe } from 'fp-ts/function';
import * as A from 'fp-ts/NonEmptyArray';
import Either = E.Either;
, , Either' â , throw
, (Left) :
// :
const validateAtSign = (email: string): string => {
if (!email.includes('@')) {
throw new AtSignMissingError('Email must contain "@" sign');
}
return email;
};
// :
const validateAtSign = (email: string): Either<AtSignMissingError, string> => {
if (!email.includes('@')) {
return E.left(new AtSignMissingError('Email must contain "@" sign'));
}
return E.right(email);
};
// :
const validateAtSign = (email: string): Either<AtSignMissingError, string> =>
email.includes('@') ?
E.right(email) :
E.left(new AtSignMissingError('Email must contain "@" sign'));
:
const validateAddress = (email: string): Either<LocalPartMissingError, string> =>
email.split('@')[0]?.length > 0 ?
E.right(email) :
E.left(new LocalPartMissingError('Email local-part must be present'));
const validateDomain = (email: string): Either<ImproperDomainError, string> =>
/\w+\.\w{2,}/ui.test(email.split('@')[1]) ?
E.right(email) :
E.left(new ImproperDomainError('Email domain must be in form "example.tld"'));
const validatePassword = (pwd: string): Either<EmptyPasswordError, string> =>
pwd.length > 0 ?
E.right(pwd) :
E.left(new EmptyPasswordError('Password must not be empty'));
handler
. chainW
â chain
, (type widening). , , fp-ts:
W
type Widening â . , Either/TaskEither/ReaderTaskEither , -:
// , A, B, C, D, E1, E2, E3, // foo, bar, baz, : declare const foo: (a: A) => Either<E1, B> declare const bar: (b: B) => Either<E2, C> declare const baz: (c: C) => Either<E3, D> declare const a: A; // , chain Either: const willFail = pipe( foo(a), E.chain(bar), E.chain(baz) ); // : const willSucceed = pipe( foo(a), E.chainW(bar), E.chainW(baz) );
-
T
â Tuple (,sequenceT
), ( EitherT, OptionT ). -
S
structure â ,traverseS
sequenceS
, « â ». -
L
lazy, .
â , apSW
: ap
Apply, type widening , .
handler
. chainW
, - AppError:
const handler = (email: string, pwd: string): Either<AppError, Account> => pipe(
validateAtSign(email),
E.chainW(validateAddress),
E.chainW(validateDomain),
E.chainW(validEmail => pipe(
validatePassword(pwd),
E.map(validPwd => ({ email: validEmail, password: validPwd })),
)),
);
? -, handler
â Account, AtSignMissingError, LocalPartMissingError, ImproperDomainError, EmptyPasswordError. -, handler
â Either , , , - .
NB: , â . TypeScript JavaScript , :
const bad = (cond: boolean): Either<never, string> => { if (!cond) { throw new Error('COND MUST BE TRUE!!!'); } return E.right('Yay, it is true!'); };
, , . , , Either/IOEithertryCatch
, âTaskEither.tryCatch
.
â . -, Option, , , . .
Either - â Validation. -, , â . , Validation , E
concat :: (a: E, b: E) => E
Semigroup. Validation Either , . , ( handler
) , , (validateAtSign, validateAddress, validateDomain, validatePassword).
: NonEmptyArray ( ) , . lift
, A => Either<E, B>
A => Either<NonEmptyArray<E>, B>
:
const lift = <Err, Res>(check: (a: Res) => Either<Err, Res>) => (a: Res): Either<NonEmptyArray<Err>, Res> => pipe(
check(a),
E.mapLeft(e => [e]),
);
, , sequenceT
fp-ts/Apply:
import { sequenceT } from 'fp-ts/Apply';
import NonEmptyArray = A.NonEmptyArray;
const NonEmptyArraySemigroup = A.getSemigroup<AppError>();
const ValidationApplicative = E.getApplicativeValidation(NonEmptyArraySemigroup);
const collectAllErrors = sequenceT(ValidationApplicative);
const handlerAllErrors = (email: string, password: string): Either<NonEmptyArray<AppError>, Account> => pipe(
collectAllErrors(
lift(validateAtSign)(email),
lift(validateAddress)(email),
lift(validateDomain)(email),
lift(validatePassword)(password),
),
E.map(() => ({ email, password })),
);
, , :
> handler('user@host.tld', '123') { _tag: 'Right', right: { email: 'user@host.tld', password: '123' } } > handler('user_host', '') { _tag: 'Left', left: AtSignMissingError: Email must contain "@" sign } > handlerAllErrors('user_host', '') { _tag: 'Left', left: [ AtSignMissingError: Email must contain "@" sign, ImproperDomainError: Email domain must be in form "example.tld", EmptyPasswordError: Password must not be empty ] }
Dans ces exemples, je souhaite attirer votre attention sur le fait que nous obtenons un traitement diffĂ©rent du comportement des fonctions qui constituent l'Ă©pine dorsale de notre logique mĂ©tier, sans affecter les fonctions de validation elles-mĂȘmes (c'est-Ă -dire la logique mĂ©tier mĂȘme). Le paradigme fonctionnel consiste prĂ©cisĂ©ment Ă assembler Ă partir des blocs de construction existants ce qui est requis pour le moment sans avoir besoin d'une refactorisation complexe de l'ensemble du systĂšme.
Ceci conclut l'article actuel, et dans le prochain nous parlerons de Task, TaskEither et ReaderTaskEither. Ils nous permettront de se faire une idée des effets algébriques et de comprendre ce que cela donne en termes de facilité de développement.