Introdução
Hoje nós vamos militar sobre o uso de tipos e aproveitar também para falar um pouco sobre Tipos de dados Algébricos (ADTs). Tentaremos desmistificar esses monstros de nossas cabeças.
Por que tipos???
Quando estamos em linguagens tipadas, nosso maior aliado são os tipos. Expressar tipos no nosso código é de extrema importância pois ela é visualizada pelo nosso compilador. Dessa forma, podemos ter nosso código otimizado em cima dessas expressões como também podemos ter uma checagem em tempo de compilação de nosso código. Deixando-o assim mais seguro. Por exemplo:
let a = 2;
let b = true;
let c = b / a; // The type 'int' does not match the type 'bool'
No código acima estamos tentando fazer uma operação com tipos que não conversam, a grande vantagem é que antes de executar esse código, meu compilador já consegue me indicar que estou tentando fazer uma operação não permitida.
A questão é que somente esses tipos básicos não são suficientes. Por exemplo:
let balance = 200; // Meu saldo na conta em dólar
let transaction = 300; // Recebi um pagamento em real
let newBalance = balance + transaction; // 500
Obviamente o exemplo acima está errado. Para calcular meu saldo eu preciso fazer alguma conversão de moeda para somente depois fazer a operação, mas por
que meu compilador não me ajudou nessa questão??
Meu compilador não consegue ver a diferença de tipos entre o pagamento e o saldo pois eu estou usando int em ambos os casos, só que nesse caso o meu tipo não deveria ser int, na verdade no meu saldo eu uso o tipo dólar e o pagamento foi do tipo real. Em uma linguagem como FSharp, podemos criar alguns tipos unitários para poder lidar melhor com nosso operação:
[<Measure>] type dollar
[<Measure>] type real
let balance = 200<dollar>;
let transaction = 300<real>;
let newBalance = balance + transaction; // The unit of measure 'real' does not match the unit of measure 'dollar'
Ter linguagens fortemente tipadas pode ser muito benéfico para nosso uso diário.
Tipos avançados
O problema do sistema de unidades é uma simplificação do por que usar tipos (temos vários outros cases que podem ser utilizados) e de fato aquele problema em questão poderia ser resolvido no mundo das classes (uma classe de dinheiro, uso de herança e implementação de métodos para interação entre eles), mas em langs fortemente tipadas do mundo funcional não utilizamos essa abordagem.
Um dos lemas mais importantes da programação funcional é o uso de composição. Essa característica não é somente para organização de nossas funções, mas também uma abordagem para lidar com nossos tipos.
Dessa forma, muitas linguagens permitem fazer composição de tipos por meio de duas propriedades bem simples (Soma e Produto)
Soma de tipos
Nessa abordagem ao construir um tipo, nós unimos o conjunto de possibilidades, somando cada variante como foi o caso do Maybe Monad apresentado no nosso último artigo:
type Option = None | Some of string
O operador |
é como se fosse um operador de soma para tipos. Um exemplo bem legal é quando criamos um Enum:
type Size = Small | Medium | Large
Muitos artigos vão utilizar a nomenclatura de união em vez de soma (se referenciando a ideia de conjuntos), porém ambas as nomenclaturas são válidas.
Produto de tipos
Nessa abordagem ao construir um tipo nós definimos os campos que o compõem. Para exemplificar, vamos construir um tipo que represente as coordenadas de X, Y e Z de um objeto no espaço
type Coordinate = float * float * float
let value: Coordinate = (1.1, 12.3, 0.0)
Dessa forma, nosso tipo é composto dos três parâmetros onde cada parâmetro é do tipo float.
Tipos de dados Algébricos
Quando nós falamos que uma linguagem suporta tipos de dados algébricos nós estamos tentando dizer que ao criar tipos nessa linguagem podemos usar operadores de soma e de produto.
Uma outra grande vantagem na composição desses tipos é o ganho que podemos ter utilizando pattern matching:
type Coordinate = float * float * float
let value: Coordinate = (1.1, 12.3, 0.0)
let result = match value with
| (0.0,0.0,0.0) -> "Ponto de saída"
| (x, _, _) when x >= 10.0 -> "Limite do eixo X"
| (_, y, _) when y >= 10.0 -> "Limite do eixo y"
| (_, _, z) when z >= 10.0 -> "Limite do eixo Z"
| (x, y, z) -> sprintf "X: %f, Y: %f, Z: %f" x y z
O pattern matching é solução onde consigo de maneira exaustiva lidar com todos os casos possiveis. Perceba que caso eu omitisse a última opção por exemplo eu teria o seguinte warning de meu compilador:
Incomplete pattern matches on this expression. For example, the value '(_,_,1.0)' may indicate a case not covered by the pattern(s). However, a pattern rule with a 'when' clause might successfully match this value.
O compilador não só me ajuda a lidar com os tipos corretos, mas agora também me ajuda a lidar com meu validador.
ADTs são muito boas para construção de sintaxes abstratas:
type Expression = Number of int
| Add of Expression * Expression
| Subtract of Expression * Expression
| Mult of Expression * Expression
| Divide of Expression * Expression
let test = Mult (Add (Number 1, Number 3), Subtract (Number 2, Number 1))
let rec extract =
function
| Number v -> v.ToString()
| Subtract (a, b) -> sprintf "(%s - %s)" (extract a) (extract b)
| Mult (a, b) -> sprintf "(%s * %s)" (extract a) (extract b)
| Add (a, b) -> sprintf "(%s + %s)" (extract a) (extract b)
| Divide (a, b) -> sprintf "(%s / %s)" (extract a) (extract b)
printf "%s" extract test // ((1 + 3) * (2 - 1))
No futuro a gente pode conversar sobre tipos de dados algébricos generalizados (GADTs) que ainda não são suportados por FSharp, mas são bem utilizados no mundo de Haskell.
Siga meu blog para mais informações joshuapassos.dev
Links bacanas sobre o assunto
- Link muito instrutivo do School of Haskell explicando sobre ADTs
- Issue pedindo a implementação de GADTs em FSharp
- Tópico do Stackoverflow trazendo a diferença da abordagem de ADTs sobre Classes
- Link do wikipedia trazendo não somente a definição como exemplos e lista de linguagens que implementam ADTs
Top comments (0)