Programação funcional em TypeScript: opção e qualquer um

Artigos anteriores da série:







  1. Polimorfismo de gênero de ordem superior
  2. Padrão de Typeclass





No artigo anterior, examinamos o conceito de classe de tipo e nos familiarizamos brevemente com as classes de tipo "functor", "monad", "monoid". Neste artigo, prometi abordar a ideia de efeitos algébricos, mas decidi escrever sobre como trabalhar com tipos e exceções anuláveis, para que uma discussão mais aprofundada fique mais clara quando passarmos a trabalhar com tarefas e efeitos. Portanto, neste artigo, ainda voltado para desenvolvedores de FP iniciantes, quero falar sobre uma abordagem funcional para resolver alguns dos problemas de aplicativo com os quais você precisa lidar todos os dias.







Como sempre, ilustrarei exemplos usando estruturas de dados da biblioteca fp-ts .







Já se tornou um tanto falta de educação citar Tony Hoare com seu "erro em um bilhão" - a introdução do conceito de um ponteiro nulo para a linguagem ALGOL W. Esse erro, como um tumor, se espalhou para outras linguagens - C, C ++, Java e, finalmente, JS. A capacidade de atribuir um valor de qualquer tipo a uma variável null



leva a efeitos colaterais indesejáveis ​​ao tentar acessar por esse ponteiro - o tempo de execução lança uma exceção, portanto, o código deve ser revestido com lógica para lidar com tais situações. Acho que todos vocês conheceram (ou até escreveram) um código tipo macarrão como:







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 , :







  1. Email «@»;
  2. Email «@»;
  3. Email «@», 1 , 2 ;
  4. 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/IOEither tryCatch



, — TaskEither.tryCatch



.

— . -, Option, , , . .







Either - — Validation. -, , — . , Validation , E



concat :: (a: E, b: E) => E



Semigroup. Validation Either , . , ( handler



) , , (validateAtSign, validateAddress, validateDomain, validatePassword).







,

:







  • Magma (), — , concat :: (a: A, b: A) => A



    . .
  • concat



    , (Semigroup). , , , — .
  • (unit) — , , — (Monoid).
  • , inverse :: (a: A) => A



    , , (Group).


Groupoid hierarchy

.







, , : fp-ts Semiring, Ring, HeytingAlgebra, BooleanAlgebra, (lattices) ..







: 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
  ]
}
      
      





Nesses exemplos, quero chamar sua atenção para o fato de que obtemos processamento diferente do comportamento das funções que constituem a espinha dorsal de nossa lógica de negócios, sem afetar as próprias funções de validação (ou seja, a própria lógica de negócios). O paradigma funcional é precisamente reunir, a partir dos blocos de construção existentes, o que é necessário no momento, sem a necessidade de refatoração complexa de todo o sistema.





Isso conclui o artigo atual e, no próximo, falaremos sobre Task, TaskEither e ReaderTaskEither. Eles nos permitirão ter uma ideia dos efeitos algébricos e entender o que eles oferecem em termos de facilidade de desenvolvimento.








All Articles