Estou aprendendo Haskell em learnyouahaskell.com . Estou tendo problemas para entender construtores de tipo e construtores de dados. Por exemplo, eu realmente não entendo a diferença entre isso:
data Car = Car { company :: String
, model :: String
, year :: Int
} deriving (Show)
e isto:
data Car a b c = Car { company :: a
, model :: b
, year :: c
} deriving (Show)
Entendo que o primeiro é simplesmente usar um construtor ( Car
) para criar dados do tipo Car
. Eu realmente não entendo o segundo.
Além disso, como os tipos de dados são definidos assim:
data Color = Blue | Green | Red
se encaixam em tudo isso?
Pelo que eu entendo, o terceiro exemplo ( Color
) é um tipo que pode ser em três estados: Blue
, Green
ou Red
. Mas isso entra em conflito com a forma como entendo os dois primeiros exemplos: será que o tipo Car
pode estar apenas em um estado Car
, que pode levar vários parâmetros para criar? Se sim, como o segundo exemplo se encaixa?
Essencialmente, estou procurando uma explicação que unifique os três exemplos / construções de código acima.
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).Car :: String -> String -> Int -> Car
) para criar dados do tipoCar
. o segundo é simplesmente usar um construtor de dados (Car :: a -> b -> c -> Car a b c
) para criar dados do tipoCar a b c
.Respostas:
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.
Aqui, temos três construtores de dados.
Colour
é um tipo, eGreen
é um construtor que contém um valor de tipoColour
. Da mesma forma,Red
eBlue
são ambos construtores que constroem valores do tipoColour
. Poderíamos imaginar apimentá-lo embora!Ainda temos apenas o tipo
Colour
, masRGB
não é um valor - é uma função que leva três Ints e retorna um valor!RGB
tem o tipoRGB
é 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!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 comoO que vemos aqui é um tipo
SBTree
que contém dois construtores de dados. Em outras palavras, existem duas funções (a saberLeaf
eBranch
) que construirão valores doSBTree
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 armazenaString
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:Tipo construtores
Ambos
SBTree
eBBTree
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:
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 .Se passarmos, digamos,
Bool
como argumento paraBTree
, ele retornará o tipoBTree Bool
, que é uma árvore binária que armazenaBool
s. Substitua todas as ocorrências da variável typea
pelo tipoBool
e você pode ver por si mesmo como é verdade.Se desejar, você pode visualizar
BTree
como uma função do tipoTipos são semelhantes aos tipos -
*
indica um tipo concreto, por isso dizemos queBTree
é de um tipo concreto para um tipo concreto.Empacotando
Volte aqui um momento e tome nota das semelhanças.
Um construtor de dados é uma "função" que recebe 0 ou mais valores e retorna um novo valor.
Um construtor de tipos é uma "função" que recebe 0 ou mais tipos e devolve um novo tipo.
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 é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 deJust
, vemos queEm outras palavras,
Just
pega um valor do tipoa
e retorna um valor do tipoMaybe a
. Se olharmos para o tipo de coisaMaybe
, vemos queEm 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 executarvocê receberá um erro. No entanto, você pode criar uma lista de
Maybe Int
, ouMaybe a
. IssoMaybe
ocorre porque é uma função construtora de tipo, mas uma lista precisa conter valores de um tipo concreto.Maybe Int
eMaybe a
são tipos concretos (ou, se desejar, chamadas para digitar funções construtoras que retornam tipos concretos.)fonte
data Colour = Red | Green | Blue
"não temos nenhum construtor" é totalmente errada. Os construtores de tipos e construtores de dados não precisam aceitar argumentos; veja, por exemplo, haskell.org/haskellwiki/Constructor, que aponta que emdata Tree a = Tip | Node a (Tree a) (Tree a)
"existem dois construtores de dados, Tip e Node".-XEmptyDataDecls
) que permite fazer isso. Como, como você diz, não há valores com esse tipo, uma funçãof :: Int -> Z
pode, por exemplo, nunca retornar (porque o que retornaria?) No entanto, elas podem ser úteis quando você deseja tipos, mas não se importa com valores .:k Z
e isso me deu uma estrela.Haskell possui tipos de dados algébricos , que poucas outras línguas possuem. Talvez seja isso que o confunda.
Em outros idiomas, geralmente você pode criar um "registro", "estrutura" ou similar, que possui vários campos nomeados que contêm vários tipos diferentes de dados. Você também pode, por vezes, fazer um "enumeração", que tem um conjunto (pequeno) de valores fixos possíveis (por exemplo, o seu
Red
,Green
eBlue
).No Haskell, você pode combinar os dois ao mesmo tempo. Estranho, mas é verdade!
Por que é chamado de "algébrico"? Bem, os nerds falam sobre "tipos de soma" e "tipos de produto". Por exemplo:
Um
Eg1
valor é basicamente quer um inteiro ou uma string. Portanto, o conjunto de todos osEg1
valores possíveis é a "soma" do conjunto de todos os possíveis valores inteiros e todos os possíveis valores de sequência. Assim, os nerds se referemEg1
como um "tipo de soma". Por outro lado:Cada
Eg2
valor consiste em ambos um inteiro e uma string. Portanto, o conjunto de todos osEg2
valores possíveis é o produto cartesiano do conjunto de todos os números inteiros e o conjunto de todas as cadeias. Os dois conjuntos são "multiplicados" juntos, então esse é um "tipo de produto".Os tipos algébricos de Haskell são tipos de soma de tipos de produtos . Você fornece a um construtor vários campos para criar um tipo de produto e possui vários construtores para fazer uma soma (de produtos).
Como um exemplo de por que isso pode ser útil, suponha que você tenha algo que produz dados como XML ou JSON e seja necessário um registro de configuração - mas, obviamente, as definições de configuração para XML e JSON são totalmente diferentes. Então você pode fazer algo assim:
(Com alguns campos adequados, obviamente.) Você não pode fazer coisas assim em linguagens de programação normais, e é por isso que a maioria das pessoas não está acostumada.
fonte
union
s, com uma disciplina de tag. :)union
, as pessoas me olham como "quem diabos usa isso ?" ;-)union
usado na minha carreira em C. Por favor, não faça parecer desnecessário, porque esse não é o caso.Comece com o caso mais simples:
Isso define um "construtor de tipos"
Color
que não aceita argumentos - e possui três "construtores de dados"Blue
,Green
eRed
. Nenhum dos construtores de dados aceita argumentos. Isto significa que existem três tipoColor
:Blue
,Green
eRed
.Um construtor de dados é usado quando você precisa criar um valor de algum tipo. Gostar:
cria um valor
myFavoriteColor
usando oGreen
construtor de dados - emyFavoriteColor
será do tipo,Color
pois esse é o tipo de valores produzido pelo construtor de dados.Um construtor de tipo é usado quando você precisa criar um tipo de algum tipo. Geralmente, esse é o caso ao escrever assinaturas:
Nesse caso, você está chamando o
Color
construtor de tipos (que não aceita argumentos).Ainda comigo?
Agora, imagine que você não apenas queria criar valores de vermelho / verde / azul, mas também queria especificar uma "intensidade". Como, um valor entre 0 e 256. Você pode fazer isso adicionando um argumento a cada um dos construtores de dados, para terminar com:
Agora, cada um dos três construtores de dados leva um argumento do tipo
Int
. O construtor de tipo (Color
) ainda não aceita argumentos. Então, minha cor favorita é um verde escuro, eu poderia escreverE, novamente, ele chama o
Green
construtor de dados e eu recebo um valor do tipoColor
.Imagine se você não quiser ditar como as pessoas expressam a intensidade de uma cor. Alguns podem querer um valor numérico como acabamos de fazer. Outros podem ficar bem com apenas um booleano indicando "brilhante" ou "não tão brilhante". A solução para isso é não codificar
Int
nos construtores de dados, mas usar uma variável de tipo:Agora, nosso construtor de tipos aceita um argumento (outro tipo que acabamos de chamar
a
!) E todos os construtores de dados aceitam um argumento (um valor!) Desse tipoa
. Então você poderia terou
Observe como chamamos o
Color
construtor de tipos com um argumento (outro tipo) para obter o tipo "efetivo" que será retornado pelos construtores de dados. Isso toca o conceito de tipos que você pode querer ler sobre uma ou duas xícaras de café.Agora descobrimos o que são os construtores de dados e os construtores de tipos e como os construtores de dados podem assumir outros valores como argumentos e os construtores de tipos podem usar outros tipos como argumentos. HTH.
fonte
->
na assinatura.a
indata Color a = Red a
.a
é um espaço reservado para um tipo arbitrário. No entanto, você pode ter o mesmo em funções simples, por exemplo, uma função do tipo(a, b) -> a
usa uma tupla de dois valores (dos tiposa
eb
) e gera o primeiro valor. É uma função "genérica" na medida em que não determina o tipo dos elementos da tupla - apenas especifica que a função gera um valor do mesmo tipo que o primeiro elemento da tupla.Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.
Isso é muito útil.Como outros apontaram, o polimorfismo não é tão útil aqui. Vejamos outro exemplo com o qual você provavelmente já está familiarizado:
Este tipo possui dois construtores de dados.
Nothing
é um pouco chato, não contém dados úteis. Por outro lado,Just
contém um valor dea
- qualquer que seja o tipoa
. Vamos escrever uma função que use esse tipo, por exemplo, obter o cabeçalho de umaInt
lista, se houver alguma (espero que você concorde que isso seja mais útil do que gerar um erro):Portanto, neste caso,
a
é umInt
, mas funcionaria bem para qualquer outro tipo. De fato, você pode fazer nossa função funcionar para todos os tipos de lista (mesmo sem alterar a implementação):Por outro lado, você pode escrever funções que aceitam apenas um certo tipo de
Maybe
, por exemplo,Para encurtar a história, com o polimorfismo, você oferece ao seu tipo a flexibilidade de trabalhar com valores de outros tipos diferentes.
No seu exemplo, você pode decidir em algum momento que
String
não é suficiente para identificar a empresa, mas ela precisa ter seu próprio tipoCompany
(que contém dados adicionais como país, endereço, contas bancárias etc.). Sua primeira implementaçãoCar
precisaria ser alterada para ser usada emCompany
vez doString
primeiro valor. Sua segunda implementação está ótima, você a usa como antesCar Company String Int
e funcionaria como antes (é claro que as funções de acesso aos dados da empresa precisam ser alteradas).fonte
data Color = Blue ; data Bright = Color
? Eu tentei em ghci, e parece que o Color no construtor de tipo não tem nada a ver com o construtor de dados Color na definição Bright. Existem apenas dois construtores de cores, um que é Data e o outro é Type.data
ornewtype
(por exemplodata Bright = Bright Color
), ou você pode usartype
para definir um sinônimo (por exemplotype Bright = Color
).O segundo tem a noção de "polimorfismo".
O
a b c
pode ser de qualquer tipo. Por exemplo,a
pode ser um[String]
,b
pode ser[Int]
ec
pode ser[Char]
.Enquanto o primeiro tipo é fixo: empresa é a
String
, modelo é aString
e ano éInt
.O exemplo Car pode não mostrar o significado do uso de polimorfismo. Mas imagine que seus dados são do tipo lista. Uma lista pode conter
String, Char, Int ...
Nessas situações, você precisará da segunda maneira de definir seus dados.Quanto à terceira maneira, acho que não precisa se encaixar no tipo anterior. É apenas uma outra maneira de definir dados no Haskell.
Esta é a minha humilde opinião como iniciante.
Btw: Certifique-se de treinar bem o seu cérebro e se sentir confortável com isso. É a chave para entender o Monad mais tarde.
fonte
É sobre tipos : no primeiro caso, você define os tipos
String
(por empresa e modelo) eInt
por ano. No segundo caso, você é mais genérico.a
,,b
ec
podem ser os mesmos tipos do primeiro exemplo ou algo completamente diferente. Por exemplo, pode ser útil fornecer o ano como string em vez de inteiro. E se você quiser, você pode até usar seuColor
tipo.fonte