Programação Funcional

Diferentes paradigmas

  • Imperativo: Assembly
  • Procedural: ALGOL, C, Fortran
  • Orientado à Objetos: Smalltalk, Java
  • Declarativo: SQL, HTML, CSS
  • Lógico: Prolog, Datalog
  • Funcional

Uma breve história

  • Cálculo Lambda.
  • LISP de McCarthy.
  • Java 8+.
  • Haskell, Elixir, Scheme, Haskell, Idris, Agda, …

O que é

  • Programas compostos por composição de funções.
  • Minimização de efeitos colaterais.
  • Declarativo, não imperativo.

Composição de funções

  • Funções são cidadãos de primeira classe.
  • Ou seja: são valores como quaisquer outros.
  • Permite que sejam declaradas em escopo local, passadas como parâmetros ou como valores de retorno, etc.

Minimização de efeitos colaterais

  • Operam somente com os argumentos que foram passados.
  • Não conseguem afetar o ambiente (inclui banco de dados, requisições HTTP e até printar no console).

A importância de funções puras

  • Permitem que comportamentos sejam observados de maneira isolada.
  • Mais fácil de testar.
  • Mais fácil de provar que uma implementação é correta.

Mas se é impossível se comunicar com o ambiente, como fazer qualquer coisa útil?

Usando funções impuras :(

Mas minimizando a quantidade e o escopo delas.

Núcleo funcional, casca imperativa.

Nós já usamos programação funcional

  • JavaScript: Array.map, Array.filter, Array.reduce, etc.
  • Java 8+: Streams e Lambdas.
  • Kotlin: sintaxe dedicada para chamada de funções anônimas { it -> ... }.

Muito software é funcional

  • Nubank: Clojure, Scala.
  • Twitter: Scala.
  • Walmart: Scala.
  • Whatsapp: Erlang.
  • Pinterest: Elixir.
  • Jane Street: Haskell, OCaml.
  • SoundCloud: Clojure, Scala.
  • CircleCI: Clojure.
  • Metabase: Clojure.
  • Pandoc: Haskell.

Exemplos

Soma de números em um array.

function soma(array) {
  let s = 0;
  for (let i = 0; i < array.length(); i++) {
    s += array[i];
  }
  return s;
}
const soma = (array) => array.reduce((a, b) => a + b, 0);

Contar quantidade de linhas em um texto.

function linhas(texto) {
  let l = 0;
  for (let i = 0; i < texto.length(); i++) {
    if (texto[i] === '\n') {
      l++;
    }
  }
  return l;
}
const linhas = (texto) => texto.filter(c => c === '\n').length();

Um exemplo real!

// src/framework/converter/converter.ts
export interface Converter<I, O> {
  convert(input: I): O
}
type ConverterFn<I, O> = (input: I): O
export class DateConverter implements Converter<Date | string | number, string> {
  convert(input: Date | string | number): string {
    if (!input) return '';
    let dateTime: DateTime;
    if (typeof input === 'string') {
      dateTime = DateTime.fromISO(input);
    } else if (typeof input === 'number') {
      dateTime = DateTime.fromSeconds(input as number);
    } else if (input instanceof Date) {
      dateTime = DateTime.fromJSDate(input);
    } else {
      throw new Error('Invalid input type. Expecting a string or a Date object.');
    }

    return dateTime.toFormat('dd/MM/yyyy');
  }
}
const parseDateTime = (input: Date | string | number): DateTime => {
  if (typeof input === 'string') {
    return DateTime.fromISO(input);
  } else if (typeof input === 'number') {
    return DateTime.fromSeconds(input as number);
  } else if (input instanceof Date) {
    return DateTime.fromJSDate(input);
  } else {
    throw new Error('Invalid input type. Expecting a string or a Date object.');
  }
}

const dateConverter: ConverterFn<Date | string | number, string> = (input) => {
  if (!input) return '';
  return parseDateTime(input).toFormat('dd/MM/yyyy');
}

Podemos fazer melhor!

const createDateConverter = (format: string): ConverterFn<Date | string | number, string> => 
  (input) => {
    if (!input) return '';
    return parseDateTime(input).toFormat(format);
  }

const dateConverter = createDateConverter('dd/MM/yyyy');
const dateHourConverter = createDateConverter('dd/MM/yyyy HH:mm');
const bestDateConverter = createDateConverter('yyyy/MM/dd');

Removemos código duplicado!

Essa estratégia de criar funções “geradoras” que retornam funções mais específicas é chamado de aplicação parcial ou Currying.

const applyOr = <I, O>(fn1: ConverterFn<I, O>, fn2: ConverterFn<I, O>): ConverterFn<I, O> => 
  (input) => {
    const result = fn1(input);
    return Boolean(result) ? result : fn2(input);
  }

const dateConverter = createDateConverter('dd/MM/yyyy');
const dateConv1 = applyOr(dateConverter, (_) => '--');
const dateConv2 = applyOr(dateConverter, (_) => 'SEM DATA');

Um problema com essa implementação é que é difícil saber quando uma operação falhou. Isso é importante para aplicar as operações subsequentes.

Algumas linguagens funcionais resolvem esse problema com Functors, Applicatives e Monads. São conceitos relacinados à Teoria das Categorias, que é uma área da Matemática.

java.util.Optional tem uma interface funcional nos métodos map, flatMap, filter, etc.

Os tipos nulláveis do Kotlin (T?) também possuem interface semelhante.

FIM!