o coração da fubá
this post is only available in português
Terminado o manifesto, a pergunta seguinte da jhujuba veio rápido: “tá, e por onde eu começo?”. E a resposta não é HTML e nem servidor. Antes de aparecer pra alguém, a Fubá precisa existir: ter medidores, reagir a cuidado, ficar chatinha. Hoje a gente escreve ela inteira sem encostar em internet, nem HTML.
a regra de ouro
Todo programa desta série tem duas partes. A regra principal: a colelinha precisa sempre funcionar, sem saber o que é internet, HTML, banco de dados ou terminal. E a parte que conecta ela ao mundo: servidor, página, banco, rede.
Ah, e um aviso de navegação que vale pra série inteira: o código completo de cada post mora no repositório zoedsoupe/fuba. Travou? Compara o seu com o meu.
mix new fuba
mix new fuba --sup
cd fuba
Vamos ignorar por enquanto esse --sup no comando e focar no uso de mix. Se você fez os exlings, reconhece mix enquanto uma forma de interagir com projetos - uma pastinha com vários arquivos de código Elixir - e aqui ele tá criando os arquivos iniciais que vamos usar pra escrever a Fubá.
a coelhinha
# lib/fuba/coelhinha.ex
defmodule Fuba.Coelhinha do
defstruct nome: "Fubá", biscoito: 3, cafeina: 3, carinho: 3, energia: 3
@type t :: %__MODULE__{
nome: String.t(),
biscoito: 0..5,
cafeina: 0..5,
carinho: 0..5,
energia: 0..5
}
end
Uma struct é um mapa com nome, campos fixos e valores padrão: %Coelhinha{} nasce com os quatro medidores em 3, uma coelhinha nova, regulada, neutra. Daqui a pouco você vai ver o poder disso: fazer casamento de padrão em %Coelhinha{} na entrada de uma função garante que só coelhinha entra nela.
O @type t embaixo não muda nada na hora de rodar. É um tipo de documentação para outras pessoas - ou você do futuro - se lembrar das “regras” da coelinha, que nesse caso são medidores que vão de 0 a 5, nunca mais, nunca menos.
Com isso, já pode brincar no terminal:
$ iex -S mix
iex> %Fuba.Coelhinha{}
%Fuba.Coelhinha{nome: "Fubá", biscoito: 3, cafeina: 3, carinho: 3, energia: 3}
cuidando dela (sem encostar nela)
Recap rápido da imutabilidade: ninguém “muda” a coelhinha. As funções recebem uma e devolvem outra. %{c | biscoito: 5} não edita c, cria uma cópia com o campo novo, e a original segue intacta. Parece desperdício; na prática é o que te dá segurança de alterar dados sem corrompê-los.
A primeira ação de cuidado:
# lib/fuba/cuidado.ex
defmodule Fuba.Cuidado do
@moduledoc """
O coração da Fubá: 100% puro, zero IO, HTTP, HTML.
"""
alias Fuba.Coelhinha
@spec dar_biscoito(Coelhinha.t()) :: Coelhinha.t()
def dar_biscoito(%Coelhinha{} = c) do
%{c | biscoito: limita(c.biscoito + 2)}
end
end
Três coisas aí. O @moduledoc é a regra de ouro da seção anterior escrita no topo do arquivo, pra ninguém esquecer. O %Coelhinha{} = c na cláusula é o casamento de padrão: qualquer coisa que não seja uma coelhinha nem entra. E o @spec é como o @type do módulo da Fubá, só que para funções, que a gente definiu na struct - “recebe Coelhinha, devolve Coelhinha”. De novo: documentação. Você vai ver um em cima de cada função daqui pra frente.
Falta o limita/1, que mora no fim do módulo:
def limita(n) do
n |> max(0) |> min(5)
end
É ele que segura a regra dos medidores: nunca passa de 5, nunca desce de 0. O pipe joga o número no max/2 e depois no min/2, nessa ordem.
Agora é com você. Escreve as outras três ações antes de passar pro próximo passo: dar_cafe/1 (cafeína +2), fazer_cafune/1 (carinho +2) e dar_espaco/1 - essa última mexe em dois medidores: energia +2, carinho −1. Espaço recarrega, mas afasta um tiquinho. Tenta aí.
…
Escreveu mesmo né? Tô de olho ksks
…
Foi? Então confere:
def dar_cafe(%Coelhinha{} = c), do: %{c | cafeina: limita(c.cafeina + 2)}
def fazer_cafune(%Coelhinha{} = c), do: %{c | carinho: limita(c.carinho + 2)}
def dar_espaco(%Coelhinha{} = c) do
%{c | energia: limita(c.energia + 2), carinho: limita(c.carinho - 1)}
end
calculando o humor
O humor da Fubá é calculado a partir medidores, nunca armazenado. Podia ser um campo na struct, e aí moraria uma armadilha: dois lugares guardando a mesma verdade, e um deles mentindo cedo ou tarde. Calculado na hora, ele nunca mente.
@type humor :: :feliz | :chatinha | :desregulada | :go_queen
@spec humor(Coelhinha.t()) :: humor()
def humor(%Coelhinha{} = c) do
medidores = [c.biscoito, c.cafeina, c.carinho, c.energia]
cond do
Enum.count(medidores, &(&1 == 0)) >= 2 -> :desregulada
Enum.all?(medidores, &(&1 >= 4)) -> :go_queen
Enum.any?(medidores, &(&1 == 0)) -> :chatinha
true -> :feliz
end
end
O cond faz vários testes de verificação e para na primeira linha que der “verdade” - então a ordem importa: :desregulada (dois ou mais medidores zerados) precisa vir antes de :chatinha (algum zerado), senão a chatinha sempre vai ser “verdade” primeiro e a desregulada nunca acontece. O true no fim faz o papel de else (“se não…”): chegou até ali, é :feliz. E o @type humor ali em cima é só a lista dos humores possíveis, escrita como tipo - o @spec usa ela no lugar de soletrar átomo por átomo.
Do forma que a gente fez, signfica que dada uma coelinha desregulada, a função humor/1 sempre vai devolver o resultado :desregulada, não depende de nenhum contexto externo, apenas dos medidores da própria Fubá.
a regra emocional
Falta a peça que dá personalidade a ela:
@type acao :: :biscoito | :cafe | :cafune | :espaco
@spec aplicar(Coelhinha.t(), acao()) :: Coelhinha.t()
def aplicar(%Coelhinha{} = c, acao) do
if humor(c) == :desregulada and acao != :espaco do
c
else
case acao do
:biscoito -> dar_biscoito(c)
:cafe -> dar_cafe(c)
:cafune -> fazer_cafune(c)
:espaco -> dar_espaco(c)
end
end
end
Desregulada, ela não aceita biscoito, café nem cafuné: a função devolve ela inalterada. Só espaço atravessa.. Um dia a “tela bonitinha” vai chamar aplicar/2, mas quem decide é sempre esse módulo. Telinha nenhuma passa por cima da coelhinha.
carinhas
Último módulo de hoje, e o mais sério de todos:
# lib/fuba/humor.ex
defmodule Fuba.Humor do
alias Fuba.Cuidado
@spec carinha(Cuidado.humor() | atom()) :: String.t()
def carinha(:feliz), do: "( ᵔ ᴥ ᵔ )"
def carinha(:chatinha), do: "(  ̄^ ̄)"
def carinha(:desregulada), do: "( ; ᴥ ; )"
def carinha(:go_queen), do: "\\(ᵔᴥᵔ)/"
def carinha(_desconhecido), do: "( o.o )"
end
Casamento de padrão em múltiplas cláusulas de novo: uma cláusula por humor, e a última casa qualquer coisa - humor desconhecido ganha carinha neutra em vez de erro. O @spec reusa o tipo humor que a gente definiu no Cuidado, com um | atom() em cima por causa dessa cláusula final generosa. E óbvio que as carinhas são as minhas; as suas você inventa.
provando que funciona
O mix new já deixou o ExUnit arrumado, então é só escrever test/fuba/cuidado_test.exs:
defmodule Fuba.CuidadoTest do
use ExUnit.Case
alias Fuba.{Coelhinha, Cuidado}
describe "aplicar/2" do
test "desregulada ignora cafuné" do
fuba = %Coelhinha{biscoito: 0, cafeina: 0}
assert Cuidado.aplicar(fuba, :cafune) == fuba
end
end
end
O describe agrupa testes da mesma função - organização, quando criarmos mais testes vai fazer bem mais sentido. Um teste eu te dou. Os outros são seus, onde:
-
humor/1pros quatro casos-
devolver
:desreguladacom 2 medidores em 0 -
devolver
:go_queenquando todos os medidos >= 4 -
devolver
:chatinhaquando pelo menos 1 medidor em 0 -
devolver
:feliznos outros casos
-
devolver
-
desregulada aceitando
:espaco(energia em 1 vai a 3) -
limita/1não passando de 5 nem descendo de 0.
Escreve eles antes de me dar razão - depois confere os seus com os do repositório.
fechou?
Dois critérios, como combinado:
-
mix testverde no terminal - com os testes que você escreveu, não só o meu. -
No
iex -S mix, os quatro humores respondem:
iex> alias Fuba.{Coelhinha, Cuidado}
iex> Cuidado.humor(%Coelhinha{})
:feliz
iex> Cuidado.humor(%Coelhinha{biscoito: 0})
:chatinha
iex> Cuidado.humor(%Coelhinha{biscoito: 0, cafeina: 0})
:desregulada
iex> Cuidado.humor(%Coelhinha{biscoito: 5, cafeina: 5, carinho: 5, energia: 5})
:go_queen
Verde nos dois? Fechou.
A Fubá existe. Você conversa com ela no terminal, dá biscoito, vê o humor mudar. Só que ninguém além de você vê qualquer coisa - ela mora trancada num terminal. No próximo post a gente dá voz a ela: HTTP, e o primeiro servidor que você escreve na vida.