Tutorial: dois pesos, duas medidas
by Diego Navarro ~ September 13th, 2006. Filed under: Immanence.Cipitá é uma cidade do interior, dessas de novela. Cipitá fica a oeste de tudo; não há lei a oeste de Trilopinha, não há Deus a oeste de Rabidinga, e ao redor de Cipitá não existe nem água encanada.
A autoridade máxima de Cipitá é um juiz preconceituoso. Em Cipitá, ou você é cipitense, ou é um forasteiro.
data Pessoa a b = Forasteiro a | Cipitense b
O juiz preconceituoso de Cipitá tem critérios completamente diferentes para Forsteiros e Cipitenses; para um dado veredito a pronunciar seu raciocínio é o seguinte:
veredito sentença_ruim sentença_boa (Forasteiro a) = sentença_ruim a veredito sentença_ruim sentença_boa (Cipitense a) = sentença_boa a
Por exemplo, em um caso de roubo de galinhas, manda a lei consuetudinária de Cipitá que os cipitenses sejam condenados a um mês de reclusão, mas os forasteiros ficam presos três anos!
roubou_galinhas = veredito (const "3 anos") (const "1 mês")
Sejam, por exemplo, duas pessoas envolvidas com um roubo de galinhas.
vladimir = Forasteiro "Vladimir Trilopinhense" mutema = Cipitense "Mutema Bebum"
O juiz vai decidir o que fazer com ambas pessoas, usando a avançada tecnologia de programação funcional disponível hoje em dia com o Haskell:
> roubou_galinhas mutema 1 mês > roubou_galinhas vladimir 3 anos
Em Cipitá, existem dois pesos e duas medidas.
Vamos entender como funcionam as sentenças e porque usamos a palavra const. Nós definimos em veredito que cada sentença é uma alteração sobre a situação presente da pessoa em questão. Traduzindo pra Haskell, a declaração de tipo de veredito é
veredito :: (a -> j) ->
(b -> t) ->
Pessoa a b ->
j
Nessa declaração, a e b são as situações iniciais de um Forasteiro e de um Cipitense, respectivamente. (a->j) é uma função que transforma essa situação inicial num veredito. Nós definimos Pessoa a b anteriormente. O resultado final da operação toda é um veredito. A declaração de tipo de roubou_galinhas é, como esperado,
roubou_galinhas :: Pessoa a b ->
String
Uma simplificação possível teria sido evitar esse caminho indireto de “mudança de situação” e definir penas constantes:
pena sentença_ruim sentença_boa (Forasteiro a) = sentença_ruim pena sentença_ruim sentença_boa (Cipitense a) = sentença_boa
Esta simplificação permitira uma definição mais concisa de roubou_galinhas:
roubou_galinhas' = pena "3 anos" "1 mês"
Este ganho putativo em concisão traz em troca uma perda em flexibilidade. Por exemplo, o povo pode ter duas reações a uma pessoa:
viva p = "Viva "++p++", bom companheiro!" xô p = "Fora "++p++", homem mal-cheiroso e indesejável!"
Assim, o veredito do preconceituoso povo cipitense quanto a uma pessoa pode ser expresso como
gritos = veredito xô viva
Os gritos do povo são os previstos:
> gritos vladimir "Fora Vladimir Trilopinhense, homem mal-cheiroso e indesejável!" > gritos mutema "Viva Mutema Bebum, bom companheiro!"
Um juiz da capital tenderá a julgar de maneira diferente, no entanto, e expedir uma liminar anulando os efeitos do veredito do juiz de Cipitá, com a mesma sentença para o Forasteiro e o Cipitense. Assim, ele envia o seguinte programa para o juiz de Cipitá, que deve executá-lo em obediência à hierarquia de tribunais.
liminar sentença_justa = veredito sentença_justa sentença_justa
Na capital, o juiz se ocupa primariamente de analisar alterações nas penas de presos já no sistema prisional. O sistema dele tem dispositivos parecidos com o de Cipitá:
data Preso a b = MauComportamento a | BomComportamento b infracaoSimples ajusteSevero ajusteLeve (MauComportamento a) = ajusteSevero a infracaoSimples ajusteSevero ajusteLeve (BomComportamento a) = ajusteLeve a
À maneira de Cipitá, este sistema pode ser usado para julgar os presos de acordo com seu tipo. Sejam, por exemplo, dois presos e os meses que falta para que saiam:
arrependido_evangelico = BomComportamento 24 rapper_irritante = MauComportamento 24
Seja também uma infração leve, como uma briga no refeitório:
brigou = infracaoSimples (+3) (+1)
Aqui, (+3) e (+1) são funções que adicionam, respectivamente, três meses e um mês à pena do preso em questão. Para fixação, vemos como fica a pena dos presos depois da briga.
> brigou arrependido_evangelico 25 > brigou rapper_irritante 27
Evidentemente, a declaração de tipo de infracaoSimples e brigou são, simplesmente
infracaoSimples :: (a->j) -> (b->j) -> Preso a b -> j brigou :: Preso Integer Integer -> Integer
Existem, no entanto, infrações mais severas. Uma fuga da prisão certamente elimina o status de BomComportamento. Assim, uma infração severa deve não só aumentar a pena do preso, mas alterar seu tipo. Em Haskell,
infracaoSevera para_o_mau para_o_bom (BomComportamento a) = MauComportamento (para_o_bom a) infracaoSevera para_o_mau para_o_bom (MauComportamento a) = MauComportamento (para_o_mau a)
Assim, temos, num caso hipotético de fuga,
fugiu = infracaoSevera (+72) (+36)
A interação do juiz com o programa, ao decidir a nova situação dos presos será parecida com o seguinte:
> fugiu rapper_irritante MauComportamento 96 > fugiu arrependido_evangelico MauComportamento 60
A declaração de fugiu é:
fugiu :: Preso Integer Integer -> Preso Integer b
Vemos que esta função tem a interessante propriedade de ter como saída o mesmo tipo da entrada. Assim, podemos ver o que acontece se os nossos presos fugirem duas vezes fazendo simplesmente
> (fugiu . fugiu) arrependido_evangelico MauComportamento 132 > (fugiu . fugiu) rapper_irritante MauComportamento 168
O sistema simples da cidade de Cipitá está implementado no módulo Data.Either do compilador GHC de Haskell. Pessoa é o tipo de dados Either, Forasteiro e Cipitense são os construtores de dados Left e Right e veredito é a função either.
O sistema em que o “lado” (ou estado) em que um indivíduo está pode mudar em uma função não existe como módulo por default nos compiladores atuais de Haskell, mas vimos no exemplo acima como é simples implementar tipos de dados com propriedades interessantes.
Em termos mais abstratos, definimos as seguintes extensões:
module EitherExts where import Data.Either either', trigger, trigger_, switch :: (a -> b) -> (a -> b) -> Either a a -> Either b b either' f g (Left x) = Left (f x) either' f g (Right x) = Right (g x) trigger f g (Left x) = Left (f x) trigger f g (Right x) = Left (g x) trigger_ f g (Left x) = Right (f x) trigger_ f g (Right x) = Right (g x) switch f g (Left x) = Right (f x) switch f g (Right x) = Left (g x) sure :: (a->b) -> Either a a -> b sure f = either f f sure' :: (a->b) -> Either a a -> Either b b sure' f = either' f f
Este módulo contém as extensões ao tipo Data.Either necessárias para lidar com mudanças de estado em sentido genérico (por exemplo, comida crua e cozida, amigo e inimigo, sólido e fluido, tempo chuvoso ou seco, etc.), sem maiores referências ao sistema prisional.
A prisão pode querer manter registros mais completos sobre o preso. Assim, podemos querer definir um novo módulo específico para o sistema prisional:
module Prisão where
import Data.Either
import EitherExts
type Pena = Int
data Ficha = Ficha { nome :: String,
pena :: Either Pena Pena }
Isto cria pra nós funções de acesso da forma nome :: Ficha -> String.
Definimos agora, ainda no módulo Prisão as categorias de ajuste de pena existentes no código penal:
infracaoLeve f g ficha = ficha { pena = either' f g (pena ficha) }
infracaoSevera f g ficha = ficha { pena = trigger f g (pena ficha) }
-- etc., etc.
Estas funções são bastante similares, e podemos abstraí-las em uma função de ajuste de pena mais geral:
ajustaPena :: (a -> b -> Either Pena Pena -> Either Pena Pena)
-> a -> b -> Ficha -> Ficha
ajustaPena modo f g ficha = ficha { pena = modo f g (pena ficha) }
Apesar da declaração complicada de tipo, esta função simplesmente aplica uma das funções de mudança de tipo (como trigger e switch) à ficha do preso. Assim, as infrações e outras situações disciplinares podem ser escritas como:
infracaoLeve = ajustaPena either' infracaoSevera = ajustaPena trigger boaAção = ajustaPena trigger_ surrealismo = ajustaPena switch
O tipo destas funções (que aliás, não precisa ser declarada; nenhuma função precisa de uma declaração explícita de tipo, mas elas ajudam a manter as idéias em ordem) é
infracaoLeve, infracaoSevera, boaAção, surrealismo :: (Pena->Pena) ->
(Pena->Pena) ->
Ficha ->
Ficha
Finalmente, definimos como açúcar sintático a seguinte funçãozinha, que é apenas a composição de funções em ordem invertida:
(&) :: (a->b) -> (b->c) -> a -> c f & g = g . f
Podemos agora escrever nosso código penal da seguinte forma:
fugiu = infracaoSevera (*2) (+36) brigou = infracaoLeve (+5) (+1) plantouFlores = boaAcao (-1) (-5) salvouAVidaDoDiretor = boaAcao (-5) (flip div 2) cipitensesTomaramBrasilia = surrealismo (*2) (*2)
A história do nosso preso pode ser agora escrita. Por exemplo, podemos ter os presos:
everaldo = Ficha { nome = "Everaldo Normal da Cunha", pena = 100 }
biza = Ficha { nome = "Ferdinando Bizarro de Sá", pena = 50 }
Exemplos do que o diretor pode fazer com o seu programa:
*Prisão> nome everaldo
"Everaldo Normal da Cunha"
*Prisão> pena biza
Left 50
*Prisão> fugiu everaldo
Ficha {nome = "Everaldo Normal da Cunha", pena = Left 136}
*Prisão> fugiu biza
Ficha {nome = "Ferdinando Bizarro de Sá", pena = Left 100}
*Prisão> (brigou & fugiu) everaldo
Ficha {nome = "Everaldo Normal da Cunha", pena = Left 137}
*Prisão> (fugiu & brigou) everaldo
Ficha {nome = "Everaldo Normal da Cunha", pena = Left 141}
*Prisão> (plantouFlores & brigou) biza
Ficha {nome = "Ferdinando Bizarro de Sá", pena = Right 50}
*Prisão> (brigou & plantouFlores) biza
Ficha {nome = "Ferdinando Bizarro de Sá", pena = Right 54}
*Prisão> (plantouFlores & fugiu) biza
Ficha {nome = "Ferdinando Bizarro de Sá", pena = Left 85}
*Prisão> (fugiu & plantouFlores) biza
Ficha {nome = "Ferdinando Bizarro de Sá", pena = Right 99}
Nota-se, por exemplo, que fugir e depois plantar flores deixa o prisioneiro no estado de “bom comportamento”, mas plantar flores e depois fugir o deixa em estado de “mau comportamento”, muito embora esta pena seja numericamente menor que a outra, devido à estrutura do código penal. Como cereja no bolo, definimos uma função de relatório:
relatorio :: Ficha -> String
relatorio ficha = "O preso " ++ nome ficha ++
" tem ainda " ++ show (sure id (pena ficha))
++ " meses de pena para cumprir"
Os relatórios podem ser arbitrariamente longos e incluir qualquer outra informação que se tenha convencionado incluir no tipo de dados Ficha. No limite, o computador pode emitir o documento legal inteiro, eliminando milhares de assim-chamados “técnicos judiciários” que custam fortunas à Viúva e à Nação!
*Prisão> relatorio everaldo "O preso Everaldo Normal da Cunha tem ainda 100 meses de pena para cumprir" *Prisão> relatorio ((fugiu & plantouFlores) biza) "O preso Ferdinando Bizarro de Sá tem ainda 99 meses de pena para cumprir"
Vimos neste tutorial como tipos abstratos de dados permitem incorporar estrutura adicional aos tipos de dados pré-existentes no computador (como os números inteiros Int) e como esta estrutura pode ir sendo propagada desde um nível muito abstrato (os módulos Data.Either e EitherExts) até uma biblioteca de combinadores (funções sobre funções retornando funções) que permite definir uma linguagem específica de domínio (aqui, uma linguagem de histórico prisional) a ser usada pelo legislador (que define uma pena para fugas, por exemplo, como fugiu = infracaoSevera (*2) (+36)) e pelo administrador (que diz simplesmente fugiu everaldo).
Bibliotecas de combinadores mais sofisticadas existem para domínios de aplicação como a música e os derivativos financeiros. Mas mesmo sistemas bem mais simples, como o descrito acima, permitem codificar o conhecimento de especialistas em seus domínios (legislação penal, por exemplo) em uma forma na qual os computadores conseguem pensar.