Mergulhando em Programação Funcional

Tentando mais uma vez falar sobre programação funcional, trazendo uma introdução detalhada dos conceitos mais importantes e fazendo um mergulho em conceitos de forma explicativa.

Assim como programação orientada a objetos, programação funcional é um paradigma que visa resolver os problemas utilizando uma forma mais orientada a funções e composição ao invés de classes e heranças.

Conceitos principais

Programação funcional faz o uso de funções puras, composições de função, tratando as funções como uma função de primeira ordem. É importante ter em mente alguns conceitos antes de começar a ter um desafio utilizando programação funcional.

Nesse tópico iremos abordar sobre

  • Imutabilidade
  • Funções puras
  • Funções de primeira ordem
  • Funções de alta ordem
  • Recursão
  • Composição

Imutabilidade

Talvez esse seja o princípio mais importante de programação funcional. Como o próprio nome sugere, imutabilidade visa a não alteração de variáveis durante o ciclo de vida numa função, evitando efeitos colaterais. Para garantir a imutabilidade, é importante utilizar funções que não alteram o estado das variáveis de entrada ou globais do sistema e sim calcular os valores com base na entrada e retornar novos valores.

Funções puras

Como dito acima em imutabilidade, as funções não devem ter efeitos colaterais, ou seja, para uma entrada X, sempre deve haver uma saída Y e não gerar nenhuma mutação em valores que não foram criados no escopo da função

Funções de primeira ordem

É importante que você pense em funções como variáveis quaisquer, onde você pode passar uma função como parâmetros de outras funções, talvez esse conceito seja conhecido por você como callback. Simplificando, funções podem ser entradas de outras funções.

Funções de alta ordem

O nome do conceito ser parecido com o nome do conceito anterior talvez não seja coincidência, já que esse conceito remete ao retorno de funções ao invés da entrada. Funções também podem ser retornadas em outras funções, fazendo uma cadeia de funções.

Recursão

<img src="/recursive-meme.png" className="w-full block min-w-full" alt="Meme recursão" />

De forma bem simplificada, recursão é a habilidade de uma função chamar ela mesmo, podendo substituir laços de repetição. Além dos laços, você pode reexecutar a função sempre que precisar atender a uma determinada execução e parar a recursão com uma condição de saída. A condição de saída é o ponto mais importante na recursão, caso você esqueça, você poderá causar uma execução infinita ou até tomar erros de Stack Overflow

Composição

Composição ao invés de herança

Essa é uma frase famosa para explicar o motivo de compor funções é melhor que herança, devido ao seu controle no fluxo e facilidade na implementação. A composição de função pode ser entendida pela notação f(g(x)). Porém, ao escrever um código, talvez isso não seja a coisa mais legível do mundo, então para isso temos algumas técnicas que nos facilitam na hora de compor funções

Hora da prática

Agora que a teoria já foi apresentada, vamos observar alguns conceitos na prática, na prática. Aqui vamos sempre lembrar que os conceitos de imutabilidade e funções puras serão sempre aplicadas, dado que são conceitos raíz

Recursão

Um problema clássico para se resolver utilizando recursão é a sequência de Fibonacci. Você pode brincar com a implementação no playground

const const fibonacci: (n: number) => numberfibonacci = (n: numbern: number): number =>
    n: numbern <= 1 ? n: numbern : const fibonacci: (n: number) => numberfibonacci(n: numbern - 1) + const fibonacci: (n: number) => numberfibonacci(n: numbern - 2);

var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(const fibonacci: (n: number) => numberfibonacci(3));

Como foi comentado anteriormente, é sempre importante ter uma condição de saída para evitar a recursão infinita

Funções de primeira ordem

Não foi comentado anteriormente, mas possivelmente você faz bastante o uso desse conceito no seu dia-a-dia. Funções como map, forEach, filter e reduce são um dos exemplos mais conhecidos desse conceito. Podemos observar em:

// utilizando map
const upper = (list: number[]) => list.map(x => x.toUpperCase());

// utilizando reducez1x
const sum = (list: number[]) => list.reduce((acc, el) => acc + el, 0);

Conceitos de programação

Pipe

De forma resumida, este conceito consiste em ser uma função agregadora de funções. Onde a saída de uma função é a entrada de outra. Por meio desse conceito é possível concatenar funções através de seu resultado, tendo assim uma pipeline de funções. Visualmente você pode entender melhor

função(argumentos) 
	-> função2(retornoFunção1)
	-> função3(retornoFunção2)
	-> retornoFunção3

Podemos ver melhor uma comparação utilizando Typescript entre uma função com pipe e uma função sem o pipe

// implementação sem pipe
const parseName = (name: string) =>
    formatBrazilianNames(capitalize(normalize(name)))


// implementação com pipe
const parseName = pipe(normalize, capitalize, formatBrazilianNames);

Para entender um pouco melhor o que nossa função pipe representa, vamos duas implementações:

  1. implementação não tipada, apenas para entender o conceito
  2. utilitário totalmente tipado, facilitando o uso do conceito e melhoria na identificação de bugs

Pipe não tipado

type Fn = (...a: any[]) => any;

const pipe = (first: A, ...fns: Fn[]) =>
    fns.reduce(
        (f: Fn, g: Fn) =>
            (...args: unknown[]) =>
                g(f(...args)), (...args: unknown[]) => a(...args));
  1. Fn: é um tipo que utilizaremos para garantir que temos apenas funções
  2. const pipe: aqui na criação da nossa função pipe, é exigido uma função e fazemos um spread de outras N funções para concatenar
  3. fns.reduce: utilizamos o reduce para agregar as funções, fazendo com que a entrada de g seja a saída de f(...args)
  4. O segundo parâmetro do nosso reduce é o inicializador, sendo esse uma função que recebe quaisquer argumentos e passa esses valores para a função first

Pipe tipado

Esse cara não vai ser explicado no artigo devido à complexidade dessa tipagem, mas você pode conferir a explicação no artigo pipe-type

import {import LL, import NN} from "ts-toolbelt";

type type Fn = (...a: any[]) => anyFn = (...a: any[]a: any[]) => any;

type type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0> = Fns["length"] extends C ? Acc : PipeArgs<Fns, Fns[C], L.Merge<Acc, [(p: ReturnType<Func>) => ReturnType<Fns[C]>]>, N.Add<C, 1>>PipeArgs<function (type parameter) Fns in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Fns extends readonly type Fn = (...a: any[]) => anyFn[], function (type parameter) Func in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Func extends type Fn = (...a: any[]) => anyFn, function (type parameter) Acc in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Acc extends readonly type Fn = (...a: any[]) => anyFn[] = [], function (type parameter) C in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>C extends number = 0> = function (type parameter) Fns in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Fns["length"] extends function (type parameter) C in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>C
    ? function (type parameter) Acc in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Acc
    : type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0> = Fns["length"] extends C ? Acc : PipeArgs<Fns, Fns[C], L.Merge<Acc, [(p: ReturnType<Func>) => ReturnType<Fns[C]>]>, N.Add<C, 1>>PipeArgs<function (type parameter) Fns in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Fns, function (type parameter) Fns in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Fns[function (type parameter) C in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>C], import LL.
type Merge<L extends List, L1 extends List, depth extends Depth = "flat", ignore extends object = BuiltIn, fill extends unknown = undefined> = Merge<L, L1, depth, ignore, fill> extends L.List ? Merge<L, L1, depth, ignore, fill> : L.List
export Merge
Accurately merge the fields of `L` with the ones of `L1`. It is equivalent to the spread operator in JavaScript. [[Union]]s and [[Optional]] fields will be handled gracefully. (⚠️ needs `--strictNullChecks` enabled)
@paramL to complete@paramL1 to copy from@paramdepth (?=`'flat'`) 'deep' to do it deeply@paramignore (?=`BuiltIn`) types not to merge@paramfill (?=`undefined`) types of `O` to be replaced with ones of `O1`@returns[[List]]@example```ts ```
Merge
<function (type parameter) Acc in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Acc, [(p: ReturnType<Func>p: type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<function (type parameter) Func in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Func>) => type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<function (type parameter) Fns in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>Fns[function (type parameter) C in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>C]>]>, import NN.
type Add<N1 extends number, N2 extends number> = N1 extends unknown ? N2 extends unknown ? _Add<IterationOf<N1>, IterationOf<N2>>[0] : never : never
export Add
Add a [[Number]] to another one
@paramN1 Left-hand side@paramN2 Right-hand side@returns`string | number | boolean`@example```ts import {N} from 'ts-toolbelt' type test0 = N.Add<'2', '10'> // '12' type test1 = N.Add<'0', '40'> // '40' type test2 = N.Add<'0', '40', 's'> // '40' type test3 = N.Add<'0', '40', 'n'> // 40 type test4 = N.Add<'-20', '40', 's'> // '20' type test5 = N.Add<'-20', '40', 'n'> // 20 ```
Add
<function (type parameter) C in type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0>C, 1>>;
type type PipeReturn<First extends Fn, Last extends Fn> = (...params: Parameters<First>) => ReturnType<Last>PipeReturn<function (type parameter) First in type PipeReturn<First extends Fn, Last extends Fn>First extends type Fn = (...a: any[]) => anyFn, function (type parameter) Last in type PipeReturn<First extends Fn, Last extends Fn>Last extends type Fn = (...a: any[]) => anyFn> = (...params: Parameters<First>params: type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
Obtain the parameters of a function type in a tuple
Parameters
<function (type parameter) First in type PipeReturn<First extends Fn, Last extends Fn>First>) => type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
Obtain the return type of a function type
ReturnType
<function (type parameter) Last in type PipeReturn<First extends Fn, Last extends Fn>Last>;
export const const pipe: <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>) => PipeReturn<A, L.Last<T>>pipe = <function (type parameter) A in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>A extends type Fn = (...a: any[]) => anyFn, function (type parameter) T in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>T extends readonly type Fn = (...a: any[]) => anyFn[]>(a: A extends Fna: function (type parameter) A in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>A, ...fns: PipeArgs<T, A, [], 0>fns: type PipeArgs<Fns extends readonly Fn[], Func extends Fn, Acc extends readonly Fn[] = [], C extends number = 0> = Fns["length"] extends C ? Acc : PipeArgs<Fns, Fns[C], L.Merge<Acc, [(p: ReturnType<Func>) => ReturnType<Fns[C]>]>, N.Add<C, 1>>PipeArgs<function (type parameter) T in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>T, function (type parameter) A in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>A>): type PipeReturn<First extends Fn, Last extends Fn> = (...params: Parameters<First>) => ReturnType<Last>PipeReturn<function (type parameter) A in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>A, import LL.
type Last<L extends List> = L[L.Length<L.Tail<L>>]
export Last
Get the last entry of `L`
@paramL to extract from@returns[[Any]]@example```ts ```
Last
<function (type parameter) T in <A extends Fn, T extends readonly Fn[]>(a: A, ...fns: PipeArgs<T, A>): PipeReturn<A, L.Last<T>>T>> =>
(fns: PipeArgs<T, A, [], 0>fns as type Fn = (...a: any[]) => anyFn[]).Array<Fn>.reduce(callbackfn: (previousValue: Fn, currentValue: Fn, currentIndex: number, array: Fn[]) => Fn, initialValue: Fn): Fn (+2 overloads)
Calls the specified callback function for all the elements in an array. The return value of the callback function is the accumulated result, and is provided as an argument in the next call to the callback function.
@paramcallbackfn A function that accepts up to four arguments. The reduce method calls the callbackfn function one time for each element in the array.@paraminitialValue If initialValue is specified, it is used as the initial value to start the accumulation. The first call to the callbackfn function provides this value as an argument instead of an array value.
reduce
(
(f: Fnf: type Fn = (...a: any[]) => anyFn, g: Fng: type Fn = (...a: any[]) => anyFn) => (...args: unknown[]args: unknown[]) => g: (...a: any[]) => anyg(f: (...a: any[]) => anyf(...args: unknown[]args)), (...args: unknown[]args: unknown[]) =>
a: A
(...a: any[]) => any
a
(...args: unknown[]args));
const const add: (a: number, b: number) => numberadd = (a: numbera: number, b: numberb: number) => a: numbera + b: numberb; const const multiplyByTwo: (a: number) => numbermultiplyByTwo = (a: numbera: number) => a: numbera * 2; const const itsMath: PipeReturn<(a: number, b: number) => number, Fn>itsMath = const pipe: <(a: number, b: number) => number, readonly Fn[]>(a: (a: number, b: number) => number, fns_0: (p: number) => ReturnType<Fn>) => PipeReturn<(a: number, b: number) => number, L.Last<readonly Fn[]>>pipe(const add: (a: number, b: number) => numberadd, const multiplyByTwo: (a: number) => numbermultiplyByTwo); const const r: anyr = const itsMath: (a: number, b: number) => ReturnType<Fn>itsMath(5, 2) var console: Consoleconsole.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
log
(const r: anyr); // 14

Either

Em linguagens como Javascript, Java, C#, Python existem as Exceptions, formas de fazer o controle de erro lançando os erros para cima e fazendo com que a função de cima na hierarquia deva tratar as exceções. Caso não seja tratada, as exceções vão subindo até elas explodirem e quebrem o seu programa com o erro não tratado.

Além desse problema, temos algumas dificuldades para tratar esses erros através de try/catch. Para tratar de uma maneira alternativa, temos o Either.

Basicamente o Either é um "empacotador" onde existem dois valores, left e right. O valor left representa os casos de erro, já o valor right os casos de sucesso. Podemos ver uma implementação do Either.

export namespace Either {
    export type 
type Either.Left<E> = {
    error: E;
    success?: undefined;
}
Left
<function (type parameter) E in type Either.Left<E>E> = { error: Eerror: function (type parameter) E in type Either.Left<E>E; success?: undefinedsuccess?: undefined };
export type
type Either.Right<S> = {
    error?: undefined;
    success: S;
}
Right
<function (type parameter) S in type Either.Right<S>S> = { error?: undefinederror?: undefined; success: Ssuccess: function (type parameter) S in type Either.Right<S>S };
type type Either<L, R> = Left<L> | Right<R>Either<function (type parameter) L in type Either<L, R>L, function (type parameter) R in type Either<L, R>R> =
type Either.Left<E> = {
    error: E;
    success?: undefined;
}
Left
<function (type parameter) L in type Either<L, R>L> |
type Either.Right<S> = {
    error?: undefined;
    success: S;
}
Right
<function (type parameter) R in type Either<L, R>R>;
export type type Either.Create<L, R> = Left<L> | Right<R>Create<function (type parameter) L in type Either.Create<L, R>L, function (type parameter) R in type Either.Create<L, R>R> = type Either<L, R> = Left<L> | Right<R>Either<function (type parameter) L in type Either.Create<L, R>L, function (type parameter) R in type Either.Create<L, R>R>; class class EitherNoValueErrorEitherNoValueError extends var Error: ErrorConstructorError { public constructor() { super(); this.Error.message: stringmessage = "EitherError"; } } const
const create: <E, S>(error: E, success: S) => {
    error: E & ({} | null);
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
create
= <
function (type parameter) E in <E, S>(error: E, success: S): {
    error: E & ({} | null);
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
E
,
function (type parameter) S in <E, S>(error: E, success: S): {
    error: E & ({} | null);
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
S
>(error: Eerror:
function (type parameter) E in <E, S>(error: E, success: S): {
    error: E & ({} | null);
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
E
, success: Ssuccess:
function (type parameter) S in <E, S>(error: E, success: S): {
    error: E & ({} | null);
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
S
) => {
if (error: Eerror !== var undefinedundefined) { return {error: E & ({} | null)error, success: undefinedsuccess: var undefinedundefined}; } if (success: Ssuccess !== var undefinedundefined) { return {success: S & ({} | null)success, error: undefinederror: var undefinedundefined}; } throw new constructor EitherNoValueError(): EitherNoValueErrorEitherNoValueError(); }; export const const Either.isLeft: <E, S>(e: Either<E, S>) => e is Left<E>isLeft = <function (type parameter) E in <E, S>(e: Either<E, S>): e is Either.Left<E>E, function (type parameter) S in <E, S>(e: Either<E, S>): e is Either.Left<E>S>(e: Either<E, S>e: type Either<L, R> = Left<L> | Right<R>Either<function (type parameter) E in <E, S>(e: Either<E, S>): e is Either.Left<E>E, function (type parameter) S in <E, S>(e: Either<E, S>): e is Either.Left<E>S>): e: Either<E, S>e is
type Either.Left<E> = {
    error: E;
    success?: undefined;
}
Left
<function (type parameter) E in <E, S>(e: Either<E, S>): e is Either.Left<E>E> => e: Either<E, S>e.error?: E | undefinederror !== var undefinedundefined;
export const const Either.isRight: <E, S>(e: Either<E, S>) => e is Right<S>isRight = <function (type parameter) E in <E, S>(e: Either<E, S>): e is Either.Right<S>E, function (type parameter) S in <E, S>(e: Either<E, S>): e is Either.Right<S>S>(e: Either<E, S>e: type Either<L, R> = Left<L> | Right<R>Either<function (type parameter) E in <E, S>(e: Either<E, S>): e is Either.Right<S>E, function (type parameter) S in <E, S>(e: Either<E, S>): e is Either.Right<S>S>): e: Either<E, S>e is
type Either.Right<S> = {
    error?: undefined;
    success: S;
}
Right
<function (type parameter) S in <E, S>(e: Either<E, S>): e is Either.Right<S>S> => e: Either<E, S>e.success?: S | undefinedsuccess !== var undefinedundefined;
export const const Either.left: <E extends unknown>(e: E) => Left<E>left = <function (type parameter) E in <E extends unknown>(e: E): Either.Left<E>E extends unknown>(e: E extends unknowne: function (type parameter) E in <E extends unknown>(e: E): Either.Left<E>E):
type Either.Left<E> = {
    error: E;
    success?: undefined;
}
Left
<function (type parameter) E in <E extends unknown>(e: E): Either.Left<E>E> =>
const create: <E, undefined>(error: E, success: undefined) => {
    error: E & ({} | null);
    success: undefined;
} | {
    success: never;
    error: undefined;
}
create
<function (type parameter) E in <E extends unknown>(e: E): Either.Left<E>E, undefined>(e: E extends unknowne, var undefinedundefined) as
type Either.Left<E> = {
    error: E;
    success?: undefined;
}
Left
<function (type parameter) E in <E extends unknown>(e: E): Either.Left<E>E>;
export const const Either.right: <S extends unknown>(s: S) => Right<S>right = <function (type parameter) S in <S extends unknown>(s: S): Either.Right<S>S extends unknown>(s: S extends unknowns: function (type parameter) S in <S extends unknown>(s: S): Either.Right<S>S):
type Either.Right<S> = {
    error?: undefined;
    success: S;
}
Right
<function (type parameter) S in <S extends unknown>(s: S): Either.Right<S>S> =>
const create: <undefined, S>(error: undefined, success: S) => {
    error: never;
    success: undefined;
} | {
    success: S & ({} | null);
    error: undefined;
}
create
<undefined, function (type parameter) S in <S extends unknown>(s: S): Either.Right<S>S>(var undefinedundefined, s: S extends unknowns) as
type Either.Right<S> = {
    error?: undefined;
    success: S;
}
Right
<function (type parameter) S in <S extends unknown>(s: S): Either.Right<S>S>;
}

Essa implementação não é totalmente fiel ao conceito real, tentei trazer uma forma um pouco simplificada para podermos entender o conceito e apresentar um pouco de Type Assertion.

Agora um pequeno exercício para aprender o Either. Primeiro vamos um request GET HTTP para exemplificar o uso de utilitários que utilizarão o Either.

type ResponseError = {
    status: number;
    message: string;
    body: unknown;
};

type ResponseSuccess<T extends unknown = unknown> = {
    body: T;
    headers: Headers;
};

export namespace Request {

    const get = async <T>(url: string, body?: unknown):
        Promise<Either.Create<ResponseError, ResponseSuccess<T>>> => {
        try {
            const response = await fetch(url, {body: JSON.stringify(body), method: "GET"});
            if (!response.ok) {
                const body = await response.json();
                return Either.error({status: response.status, message: "Error", body});
            }
            const body = await response.json();
            return Either.success({body, headers: response.headers});
        } catch (e) {
            // esse trycatch aqui é para tratar Network error
            // em casos de falta de conexão com a internet
            return Either.error({status: 0, body: null, message: "Network error"})
        }
    }
}

Agora que temos o nosso utilitário com Either, podemos aplicar em um código para observar a aplicação real do conceito.

namespace Users {
    type User = {
        id: string;
        name: string;
    };

    export const getAll = async () => {
        const response = await Response.get<Users[]>("/api/users");
        if (Either.isError(response)) {
            return [];
        }
        const users = response.right.body;
        return users;
    }
}

Com o Either, nossa função fica totalmente segura em tempo de execução, sem nenhuma Exception sendo lançada, sem nenhum fluxo de quebra. Apenas um código com um objeto que possui um formato de erro (left) e outro objeto com o formato de sucesso (right). Pode não parecer um grande ganho no primeiro momento, mas evitar as exceptions em tempo de execução vai trazer muito mais predição para o seu código.

Conclusão

Com tudo o que observamos no artigo hoje, podemos fazer um .reduce do conteúdo e assimilar melhor. Não necessariamente você precisa adotar o paradigma funcional por completo, você pode utilizar o conceito para melhorar seus hábitos de programação.

E isso é tudo, pessoal, espero que tenham gostado.