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) => number
fibonacci = (n: number
n: number): number =>
n: number
n <= 1 ? n: number
n : const fibonacci: (n: number) => number
fibonacci(n: number
n - 1) + const fibonacci: (n: number) => number
fibonacci(n: number
n - 2);
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const fibonacci: (n: number) => number
fibonacci(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:
- implementação não tipada, apenas para entender o conceito
- 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));
Fn
: é um tipo que utilizaremos para garantir que temos apenas funçõesconst pipe
: aqui na criação da nossa função pipe, é exigido uma função e fazemos um spread de outras N funções para concatenarfns.reduce
: utilizamos o reduce para agregar as funções, fazendo com que a entrada deg
seja a saída def(...args)
- 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 L
L, import N
N} from "ts-toolbelt";
type type Fn = (...a: any[]) => any
Fn = (...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[]) => any
Fn[], 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[]) => any
Fn, 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[]) => any
Fn[] = [], 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 L
L.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)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 typeReturnType<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 typeReturnType<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 N
N.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 oneAdd<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[]) => any
Fn, function (type parameter) Last in type PipeReturn<First extends Fn, Last extends Fn>
Last extends type Fn = (...a: any[]) => any
Fn> = (...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 tupleParameters<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 typeReturnType<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[]) => any
Fn, 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[]) => any
Fn[]>(a: A extends Fn
a: 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 L
L.type Last<L extends List> = L[L.Length<L.Tail<L>>]
export Last
Get the last entry of `L`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[]) => any
Fn[]).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.reduce(
(f: Fn
f: type Fn = (...a: any[]) => any
Fn, g: Fn
g: type Fn = (...a: any[]) => any
Fn) =>
(...args: unknown[]
args: unknown[]) =>
g: (...a: any[]) => any
g(f: (...a: any[]) => any
f(...args: unknown[]
args)), (...args: unknown[]
args: unknown[]) => a: A
(...a: any[]) => any
a(...args: unknown[]
args));
const const add: (a: number, b: number) => number
add = (a: number
a: number, b: number
b: number) => a: number
a + b: number
b;
const const multiplyByTwo: (a: number) => number
multiplyByTwo = (a: number
a: number) => a: number
a * 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) => number
add, const multiplyByTwo: (a: number) => number
multiplyByTwo);
const const r: any
r = const itsMath: (a: number, b: number) => ReturnType<Fn>
itsMath(5, 2)
var console: Console
console.Console.log(...data: any[]): void
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)log(const r: any
r); // 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: E
error: function (type parameter) E in type Either.Left<E>
E; success?: undefined
success?: undefined };
export type type Either.Right<S> = {
error?: undefined;
success: S;
}
Right<function (type parameter) S in type Either.Right<S>
S> = { error?: undefined
error?: undefined; success: S
success: 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 EitherNoValueError
EitherNoValueError extends var Error: ErrorConstructor
Error {
public constructor() {
super();
this.Error.message: string
message = "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: E
error: function (type parameter) E in <E, S>(error: E, success: S): {
error: E & ({} | null);
success: undefined;
} | {
success: S & ({} | null);
error: undefined;
}
E, success: S
success: function (type parameter) S in <E, S>(error: E, success: S): {
error: E & ({} | null);
success: undefined;
} | {
success: S & ({} | null);
error: undefined;
}
S) => {
if (error: E
error !== var undefined
undefined) {
return {error: E & ({} | null)
error, success: undefined
success: var undefined
undefined};
}
if (success: S
success !== var undefined
undefined) {
return {success: S & ({} | null)
success, error: undefined
error: var undefined
undefined};
}
throw new constructor EitherNoValueError(): EitherNoValueError
EitherNoValueError();
};
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 | undefined
error !== var undefined
undefined;
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 | undefined
success !== var undefined
undefined;
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 unknown
e: 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 unknown
e, var undefined
undefined) 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 unknown
s: 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 undefined
undefined, s: S extends unknown
s) 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.