Em uma data
declaração, um construtor de tipo é a coisa do lado esquerdo do sinal de igual. O (s) construtor (es) de dados são os itens do lado direito do sinal de igual. Você usa construtores de tipo onde um tipo é esperado e usa construtores de dados onde um valor é esperado.
Construtores de dados
Para simplificar, podemos começar com um exemplo de um tipo que representa uma cor.
data Colour = Red | Green | Blue
Aqui, temos três construtores de dados. Colour
é um tipo, e Green
é um construtor que contém um valor de tipo Colour
. Da mesma forma, Red
e Blue
são ambos construtores que constroem valores do tipo Colour
. Poderíamos imaginar apimentá-lo embora!
data Colour = RGB Int Int Int
Ainda temos apenas o tipo Colour
, mas RGB
não é um valor - é uma função que leva três Ints e retorna um valor! RGB
tem o tipo
RGB :: Int -> Int -> Int -> Colour
RGB
é um construtor de dados que é uma função que aceita alguns valores como argumentos e os usa para construir um novo valor. Se você fez alguma programação orientada a objetos, deve reconhecer isso. No OOP, os construtores também recebem alguns valores como argumentos e retornam um novo valor!
Nesse caso, se aplicarmos RGB
a três valores, obteremos um valor de cor!
Prelude> RGB 12 92 27
#0c5c1b
Temos construído um valor do tipo Colour
aplicando o construtor de dados. Um construtor de dados contém um valor como uma variável ou aceita outros valores como argumento e cria um novo valor . Se você já fez a programação anterior, esse conceito não deve ser muito estranho para você.
Intervalo
Se você deseja construir uma árvore binária para armazenar String
s, você pode imaginar fazer algo como
data SBTree = Leaf String
| Branch String SBTree SBTree
O que vemos aqui é um tipo SBTree
que contém dois construtores de dados. Em outras palavras, existem duas funções (a saber Leaf
e Branch
) que construirão valores do SBTree
tipo. Se você não estiver familiarizado com o funcionamento das árvores binárias, aguarde um pouco. Na verdade, você não precisa saber como as árvores binárias funcionam, apenas que essa armazena String
s de alguma maneira.
Também vemos que os dois construtores de dados usam um String
argumento - essa é a String que eles armazenam na árvore.
Mas! E se nós também quiséssemos armazenar Bool
, teríamos que criar uma nova árvore binária. Pode ser algo como isto:
data BBTree = Leaf Bool
| Branch Bool BBTree BBTree
Tipo construtores
Ambos SBTree
e BBTree
são construtores de tipo. Mas há um problema evidente. Você vê como eles são semelhantes? Isso é um sinal de que você realmente deseja um parâmetro em algum lugar.
Para que possamos fazer isso:
data BTree a = Leaf a
| Branch a (BTree a) (BTree a)
Agora, introduzimos uma variável de tipo a
como parâmetro no construtor de tipos. Nesta declaração, BTree
tornou-se uma função. Ele aceita um tipo como argumento e retorna um novo tipo .
É importante considerar aqui a diferença entre um tipo concreto (exemplos incluem Int
, [Char]
e Maybe Bool
), que é um tipo que pode ser atribuído a um valor em seu programa e uma função construtora de tipo que você precisa alimentar um tipo para poder ser atribuído a um valor. Um valor nunca pode ser do tipo "lista", porque precisa ser uma "lista de algo ". No mesmo espírito, um valor nunca pode ser do tipo "árvore binária", porque precisa ser uma "árvore binária que armazena algo ".
Se passarmos, digamos, Bool
como argumento para BTree
, ele retornará o tipo BTree Bool
, que é uma árvore binária que armazena Bool
s. Substitua todas as ocorrências da variável type a
pelo tipo Bool
e você pode ver por si mesmo como é verdade.
Se desejar, você pode visualizar BTree
como uma função do tipo
BTree :: * -> *
Tipos são semelhantes aos tipos - *
indica um tipo concreto, por isso dizemos que BTree
é de um tipo concreto para um tipo concreto.
Empacotando
Volte aqui um momento e tome nota das semelhanças.
Construtores de dados com parâmetros são legais se queremos pequenas variações em nossos valores - colocamos essas variações nos parâmetros e deixamos o cara que cria o valor decidir em quais argumentos eles serão inseridos. No mesmo sentido, digite construtores com parâmetros como legais se queremos pequenas variações em nossos tipos! Colocamos essas variações como parâmetros e deixamos o cara que cria o tipo decidir em quais argumentos eles irão colocar.
Um estudo de caso
Como o trecho da casa aqui, podemos considerar o Maybe a
tipo. Sua definição é
data Maybe a = Nothing
| Just a
Aqui Maybe
está um construtor de tipos que retorna um tipo concreto. Just
é um construtor de dados que retorna um valor. Nothing
é um construtor de dados que contém um valor. Se olharmos para o tipo de Just
, vemos que
Just :: a -> Maybe a
Em outras palavras, Just
pega um valor do tipo a
e retorna um valor do tipo Maybe a
. Se olharmos para o tipo de coisa Maybe
, vemos que
Maybe :: * -> *
Em outras palavras, Maybe
pega um tipo concreto e retorna um tipo concreto.
De novo! A diferença entre um tipo concreto e uma função construtora de tipo. Você não pode criar uma lista de Maybe
s - se tentar executar
[] :: [Maybe]
você receberá um erro. No entanto, você pode criar uma lista de Maybe Int
, ou Maybe a
. Isso Maybe
ocorre porque é uma função construtora de tipo, mas uma lista precisa conter valores de um tipo concreto. Maybe Int
e Maybe a
são tipos concretos (ou, se desejar, chamadas para digitar funções construtoras que retornam tipos concretos.)
Car
é um construtor de tipos (no lado esquerdo da=
) e um construtor de dados (no lado direito). No primeiro exemplo, oCar
construtor de tipos não usa argumentos, no segundo exemplo, três. Nos dois exemplos, oCar
construtor de dados usa três argumentos (mas os tipos desses argumentos são corrigidos em um caso e no outro parametrizados).