Uma abordagem para lidar com o fluxo de seus dados

Meu nome é Victor Igor e atualmente trabalho como desenvolvedor no IQ 360 (startup da Red Ventures) e vou lhe mostrar como a teoria das categorias pode te dar uma nova perspectiva de como modelar o fluxo de dados do seu software.

Neste post, vamos falar um pouco sobre:

  • Paradigma de programação
  • A história sobre a teoria das categorias
  • Programação Funcional
  • Como a teoria de categorias pode ajudar a construir software
  • Por que composição ?
  • Controle do fluxo
  • Como lidar com efeitos colaterais
  • Conceitos aplicados em cenários reais

Para os exemplos, usarei Javascript e PureScript.

Introdução

Nós precisamos conversar sobre paradigmas de programação.

O que é paradigma de programação para você?

Paradigma de programação é como uma abordagem para programar um computador, que é baseado em um conjunto coerente de princípios ou uma teoria matemática. Na prática, cada paradigma vem com seu próprio modo de pensar e existem problemas para os quais é a melhor abordagem.

Ilustração de um homem, olhando para o horizonte num campo gramado e com uma árvore se destacando com um brilho ao redor dela

Por exemplo:

  • Em programação funcional a teoria é chamada de Lambda calculus.
  • Em POO, existem vários tipos diferentes de teoria, mas é essencialmente teorias organizacionais de como estruturar informações e atualizar informações.
  • Multi-agent concurrent programming a teoria é chamada de pi calculus.

Razões para você estudar outras linguagens de programação

  • Novas maneiras de resolver problemas
  • Novos conceitos para aplicar em seus projetos
  • Ferramenta certa para o trabalho certo
  • Novas oportunidades
  • Mantenha se atualizado
  • The Pragmatic Programmer

Se você acha que precisa estudar apenas a linguagem em que trabalha, você precisa repensar um pouso se quiser ser um ótimo programador, porque se você aprendeu e usou apenas um estilo de linguagem de programação (oop, funcional, compilado, alto nível, baixo nível, etc) é provável que o seu pensamento em torno de programação é muito estruturado em torno dessa linguagem.

Leia um pouco mais sobre isso: If you’re only going to learn one programming language, you should learn…

A Teoria das Categorias não é um novo paradigma, mas uma nova perspectiva sobre modelagem de fluxo de dados que pode contribuir muito para o desenvolvimento de software, em especial, no que diz respeito a composição.

Uma pequena história sobre a teoria das categorias

O nascimento dessa teoria tem sido muito mais matemático do que computacional, e é muito difícil dizer exatamente quando foi a primeira vez que isso apareceu. Em 1995 foi publicado um artigo chamado “Teoria Geral das Equivalências Naturais”, onde os conceitos de categoria foi introduzido como o de functor. Os autores precisaram definir um conceito para functor e aqui chegaram a algumas conclusões de conceitos básicos a categoria.

Esses conceitos foram introduzidos muito mais na topologia algébrica, e com certeza, todo mundo sabe o que é isso! Não, eu não sei, mas vamos continuar com a nossa saga. Assim, outros avanços importantes foram publicados, mas houve um artigo que introduziu a ideia de usar a categoria para criar teorias mais gerais e aplicar em campos específicos, sim! Agora não temos apenas uma linguagem bonita, porque a categoria abstrata de estudo que encapsula os resultados algébricos homológicos também pode ser aplicada para obter resultados em outras áreas da matemática e, em seguida, vários teoremas e teorias podem ser vistos por um ângulo categórico. Mais tarde, teve uso suficiente em física teórica, a partir daí tornou-se bastante importante. Veja a timeline da teoria das categorias e matemática relacionada para você ter uma noção do tempo.

Ciência da Computação e a teoria

A teoria das categorias é um campo que afeta cada vez mais a consciência de muitos cientistas da computação, especialmente aqueles com interesse em linguagens de programação e especificações formais, mas o maior contribuinte para receber a Teoria das Categorias é Haskell e seu sistema de tipos, que estendeu o sistema do tipo Hindley-Milner com a noção de type classes.

Programação Funcional e a Teoria

Esse paper explica muito mais os conceitos da teoria aplicado na programação funcional, mas explicarei um pouco. Se você não conhece esse paradigma, recomendo que você leia este artigo: Functional Programming should be your #1 priority for 2015.

Teoria da relatividade - Planeta terra numa rede no espaço ao lado de outro planeta bem pequeno

A mudança de paradigma trazida pela teoria da relatividade de Einstein trouxe a percepção de que não há uma perspectiva única a partir da qual se possa ver o mundo. Há infinitamente muitos frameworks e perspectivas diferentes, e o poder real está em poder traduzir entre eles. É nesse contexto histórico que a teoria das categorias começou.

Como a teoria de categorias pode ajudar a construir software?

Deve ser a sua primeira pergunta quando leu a descrição deste post. A composição de funções é minha parte favorita da programação funcional, e quando comecei a estudar Haskell e PureScript, eu encontrei muitas pessoas falando sobre Abstrações e Functors. Foi nesse momento que comecei a me perguntar “por que essas pessoas estavam falando sobre isso? ” “O que é isso?”. Eu encontrei muitos artigos e livros falando sobre essa teoria, mas muitos ainda não sabem como isso pode ajudar a construir um software, e é normal acharem que isso não faz sentido.

Teoria das Categorias é sobre Composição

Sim composição! Porque para definir uma categoria, você precisa especificar qual composição está nessa categoria. É como a operação de multiplicação em um grupo (para definir um grupo, não é suficiente apenas dizer que você tem um conjunto e é possível multiplicar elementos do conjunto), você tem que realmente dizer o que entende por “multiplicar” como parte da definição do grupo.

Por que compor ?

A essência do desenvolvimento de software é a composição e as coisas que são compostas são boas porque permitem abstrações. Para saber mais por que devemos aprender sobre composição, leia a série do Eric Elliot chamada Composing Software.

Controle de fluxo

Exemplo de Control Flow

Há muitas maneiras de controlar o fluxo de seus dados, em linguagens procedurais, a computação envolve código operando em dados, em OOP, um objeto encapsula tanto código quanto dados e linguagens funcionais puras, dados não têm existência independente. RxJs é um ótimo exemplo para demonstração de como eu posso usar para trafegar dados entre funções e obter resultados no final do pipe. Na minha opinião, é uma ótima maneira de resolver um problema porque você tem um fluxo de controle definido, sem estado e, na maioria das vezes, é um código menor.


import { interval } from "rxjs";
import { map, take } from "rxjs/operators";

const bezier_stream = interval(350).pipe(
    take(25),
    map(bezier),
    map(num => "~".repeat(Math.floor(num * 65)))
);

Categoria

A Teoria das Categorias é uma teoria das funções e a única operação básica é a composição. O conceito de categoria incorpora algumas propriedades abstratas da composição.

  • Uma coleção de objetos
  • Uma coleção de morfismo
  • Para cada triplo de objetos X, Y, Z, um mapa (chamado composição)
Categorias, Objetos e Morphismo

Existem muitos tipos de categoria, mas você deve aprender sobre a categoria de conjuntos, porque é a categoria cujos objetos são conjuntos. As setas ou morfismos entre os conjuntos A e B são as funções de A a B, e a composição dos morfismos é a composição das funções.

Muitas coisas podem ser compostas

Você deve estar pensando que isso não tem nada a ver com programação e que os conceitos de matemática não podem nos ajudar, mas vamos ver alguns tópicos que temos em programação e não temos em matemática e que podemos usar a composição para nos ajudar.

  • Assignments
  • Loops
  • Erros e Callback
  • Side Effects

Assignments

Você se lembra quando eu falei sobre ‘functor’? Vamos ver por que isso pode nos ajudar.

functor exemplo

Um functor é um mapeamento entre categorias. Dadas duas categorias, A e B, um functor F mapeia objetos em A para objetos em B e para ser um functor, você deve assegurar que satisfaz as duas leis de functor:

  • Identidade => F(idA) = idF(A)
  • Composição => F(g ∘ f) = F(g) ∘ F(f)

//Identity
const F = [1, 2, 3];
F.map(x => x); // [1, 2, 3]

//Composition
F.map(x => f(g(x))) /* -> */ F.map(g).map(f)

Vamos criar nosso próprio functor para usar qualquer tipo de dados e mapeá-lo.


module Main where

import Prelude

import Control.Comonad (class Comonad, class Extend, extract)
import Control.Monad.Eff.Console (logShow)

-- Javascript - const Functor = x => ({})
newtype Box a = Box a
-- Javascript - map: f => (f(x))
instance functorBox :: Functor Box where
  map f (Box x) = Box (f x)
-- Javascript - fold: f => f(x)
instance extendBox :: Extend Box where
  extend f m = Box (f m)
instance comonadBox :: Comonad Box where
  extract (Box x) = x

app :: Int -> Int
app n =
  Box n #
  map (\n -> n * 2) #
  map (\n -> n + 1) #
  extract


main = do
  logShow $ app $ 10
---- 21

Basicamente, podemos encadear longas sequências de cálculo para resolver problemas. Eu usei o PureScript neste exemplo, mas não é útil apenas para linguagens tipadas, segue a mesma implementação em JavaScript:


const Functor = x => ({
  map: f => Functor(f(x)),
  fold: f => f(x)
})

const app = (n) => 
  Functor(n)
  .map(n => n * 2)
  .map(n => n + 1)

app(10)
  .fold(console.log) //21

Em PureScript, temos operadores para compor nossas funções:


module Main where

import Prelude

import Control.Monad.Eff.Console (logShow)

app :: Int -> Int
app =
  (\n -> n * 2) >>>
  (\n -> n + 1)

main = do
  logShow $ app $ 10
  -- 21

Neste artigo, você pode aprender mais sobre functors e categorias em JavaScript.

Loops

Looping em programação funcional não é feito com instruções de controle como for e while, é feito com chamadas explícitas para funções como map, filter e reduce ou recursão. Se o código de loop mutar variáveis fora do loop, essa função de loop interno estaria manipulando variáveis fora de seu escopo e, portanto, seria impura. Isso não é o que queremos, porque as funções impuras não são fáceis de compor. Map, filter e reduce/fold provavelmente resolve a maioria dos seus problemas relacionado a loops.


map (\n -> n + 1) [1, 2, 3, 4, 5]
-- [2, 3, 4, 5, 6]

filter (\n -> n `mod` 2 == 0) (1 .. 10)
-- [2,4,6,8,10]

foldl (\acc n -> acc <> show n) "" [1,2,3,4,5]
-- "12345"

foldr (+) 0 (1 .. 5)
-- 15

Erros e Callbacks

Existem algumas maneiras em que os erros geralmente são tratados em muitas linguagens de programação, mas basicamente usamos estruturas de fluxo de controle como if/else e exceções como try/catch. Mas qual é o problema? Estes tendem a ser difíceis de prever, difíceis de manter e compor. Basicamente, vamos usar uma estrutura de dados monádica para encapsular o fluxo de controle de maneira declarativa. Vou mostrar exemplos em JavaScript para você saber que não é restrito em linguagens fortemente tipadas.

Maybe

Impõe uma verificação nula com ramificação de código composta usando o Maybe. Uma estrutura que ajuda a lidar com valores que podem ou não estar presentes.


function Add(directory, dirWhereSaved){
  return fromNullable(directory)
    .map(() =>
         exist(directory)
         .and(createATemplate(directory, dirWhereSaved))
         .and(cp(directory)))
    .getOrElse(rejected(`You need to pass the path as a parameter`))
}

Linha 2, usei ‘fromNullable’ do Maybe, usei como uma caixa para verificar se tem valor. Estou usando o Folktale, e isso nos ajuda a compor e manter um encadeamento dos dados.

Either

Uma estrutura que modela o resultado de funções que podem falhar e essa estrutura representa a disjunção lógica entre a e b. Essa implementação específica é baseada no valor correto (b), e essas projeções terão o valor correto em relação ao valor da esquerda.


const Right = x => ({
  map: f => Right(f(x)),
  chain: f => f(x),
  fold: (f, g) => g(x)
})

const Left = x => ({
  map: f => Left(x),
  chain: f => Left(x),
  fold: (f, g) => f(x)
})

const getPackages = user =>
      (user.premium ? Right(user) : Left('not premium'))
      .map(u => u.preferences)
      .fold(() => defaultsPackages, props => loadPackages(props))
}

No Folktale, temos o Result que é semelhante.

Future

Uma estrutura de dados para representar o resultado de uma computação assíncrona.


/* Callback :( */
const doThing = () =>
  fs.readFile('codes.json', 'utf-8', (err, data) => {
    if(err) throw err 
    const newdata = data.replace(/1/g, '2')
    fs.writeFile('codes2.json', newdata, (err, _) => {
      if(err) throw err
      console.log('success!')
    }
  }
                 
/* Using future :D */
const readFile = futurize(fs.readFile)
const writeFile = futurize(fs.writefile)

const doThing = () =>
  readFile('codes.json')
  .map(data => data.replace('/1/g', '2')
  .chain(replaced => 
          writeFile('codes2.json', replaced))

doThing().fork(e => console.log(e),
               r => console.log('success'))

Agora você pode usar todos esses conceitos:


const fs = require('fs-extra')
const { createATemplate } = require('./template')
const { fromNullable } = require('folktale/maybe');
const { fromPromised, rejected, of } = require('folktale/concurrency/task');

const fsCopy = fromPromised(fs.copy)
const pathExists = fromPromised(fs.pathExists)

function cp(directory){
  return fsCopy(directory, `packblade/roles/${directory}/files/`)
}

function exist(path){
  return pathExists(path)
    .chain(exist => exist ?  of(exist)
           : /* otherwise */ rejected(`${path} not found`))
}

function Add(directory, dirWhereSaved){
  return fromNullable(directory)
    .map(() =>
         exist(directory)
         .and(createATemplate(directory, dirWhereSaved))
         .and(cp(directory)))
    .getOrElse(rejected(`You need to pass the path as a parameter`))
}

module.exports = Add

Como lidar com efeitos colaterais ?

Haskell é uma linguagem puramente funcional e todas as funções dela são puras, pra comportamentos com side-effects ela usa monads para resolver esse tipo de problema. Vamos usar um IO, então tudo que você tem de ação de IO, elas te retornam uma IO computation(dentro de uma caixa), então uma função que ler um Integer no teclado, em vez de retornar um Integer, ela retorna uma IO computation que reproduz um Integer quando for executada, e como retorna um IO, você não pode usar esse resultado diretamente em uma soma por exemplo. E pra acessar você vai ter que ‘desembrulhar’ dessa monad. É assim que Haskell isola os efeitos colaterais, a monad IO atua como uma abstração do estado do mundo real. Em Purescript temos Eff para lidar com esses tipos de efeitos colaterais, e na sua própria documentação nos diz alguns cenários de onde podemos usar.

Efeitos nativos:

  • Console IO
  • Random number generation
  • Exceptions
  • Reading/writing mutable state

E no browser:

  • DOM manipulation
  • XMLHttpRequest / AJAX calls
  • Interacting with a websocket
  • Writing/reading to/from local storage

Em JavaScript temos uma lib chamada ramda-fantasy que além de implementar algumas das abstrações anteriores, temos esse tal de IO.


import _ from 'ramda'
import { IO } from 'ramda-fantasy'

const IOwindow = IO(() => window)

IOwindow
  .map(_.prop('location'))
  .map(_.prop('href'))
  .map(_.split('/'))
// IO(["http:", "", "localhost:8000", "blog", "posts"])

const $ = selector => IO(() => document.querySelectorAll(selector))

$('#container')
  .map(_.head)
  .map(div => div.innerHTML)
// IO('I am some inner html')

Criando nosso próprio IO:


const IO = f => ({
  runIO: f,
  map: g =>
    IO(() => g(f())),
  chain: g =>
    IO(g(f()).runIO)
})


const log = label => x =>
  IO(() => (console.log(label, x), x))

const $ = x =>
  IO(() => document.querySelector(x))

const getText = e =>
  e.textContent

const main = selector =>
  $(selector)
    .map(getText)
    .chain(log('A'))
    .map(s => s.toUpperCase())
    .chain(log('B'))
    .runIO()

main('title')
// A hello world
// B HELLO WORLD

Agora você pode experimentar usar todos esses conceitos. Eu tenho uma apresentação quando falei sobre isso; Everything is composable. Esses conceitos podem ser usados em backend ou frontend, mas como programação funcional é sobre programação compondo funções, precisamos apenas de uma linguagem com funções como valores de primeira classe.

Bonus 🌟

Estou implementando um gerador de Ansible Playbook para automatizar a instalação de aplicativos e dotfiles. Você pode ver alguns desses conceitos sendo usados na prática: Packblade

Você é front-end e usa react? Divirta-se usando o react-dream usando esses e muitos outros conceitos em react.

Conclusão

Existem outros conceitos na teoria das categorias que talvez possam nos ajudar, até porque é um assunto bem vasto. A ideia foi mostrar para vocês um pouco dessa teoria e como pode nos ajudar com abstração e composição. Eu usei alguns exemplos em PureScript, que é uma linguagem fortemente tipada, escrita e inspirada em Haskell que compila para JavaScript. Talvez optar por uma linguagem puramente funcional para praticar alguns desses conceitos seja uma ótima diversão, afinal, aprendizado em teoria das categorias é um investimento a longo prazo.

Para onde devo ir?


BrazilJS é uma iniciativa NASC