Tipo Haskell vs Construtor de Dados


124

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, Greenou Red. Mas isso entra em conflito com a forma como entendo os dois primeiros exemplos: será que o tipo Carpode 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.


18
O exemplo do seu carro pode ser um pouco confuso, porque Caré um construtor de tipos (no lado esquerdo da =) e um construtor de dados (no lado direito). No primeiro exemplo, o Carconstrutor de tipos não usa argumentos, no segundo exemplo, três. Nos dois exemplos, o Carconstrutor de dados usa três argumentos (mas os tipos desses argumentos são corrigidos em um caso e no outro parametrizados).
Simon engraxar os

o primeiro é simplesmente usar um construtor de dados ( Car :: String -> String -> Int -> Car) para criar dados do tipo Car. o segundo é simplesmente usar um construtor de dados ( Car :: a -> b -> c -> Car a b c) para criar dados do tipo Car a b c.
Will Ness

Respostas:


228

Em uma datadeclaraçã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, Rede Bluesã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 RGBnão é um valor - é uma função que leva três Ints e retorna um valor! RGBtem 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 RGBa três valores, obteremos um valor de cor!

Prelude> RGB 12 92 27
#0c5c1b

Temos construído um valor do tipo Colouraplicando 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 Strings, você pode imaginar fazer algo como

data SBTree = Leaf String
            | Branch String SBTree SBTree

O que vemos aqui é um tipo SBTreeque contém dois construtores de dados. Em outras palavras, existem duas funções (a saber Leafe Branch) que construirão valores do SBTreetipo. 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 Strings de alguma maneira.

Também vemos que os dois construtores de dados usam um Stringargumento - 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 SBTreee BBTreesã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, BTreetornou-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, Boolcomo argumento para BTree, ele retornará o tipo BTree Bool, que é uma árvore binária que armazena Bools. Substitua todas as ocorrências da variável type apelo tipo Boole você pode ver por si mesmo como é verdade.

Se desejar, você pode visualizar BTreecomo 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.

  • 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 atipo. Sua definição é

data Maybe a = Nothing
             | Just a

Aqui Maybeestá 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, Justpega um valor do tipo ae retorna um valor do tipo Maybe a. Se olharmos para o tipo de coisa Maybe, vemos que

Maybe :: * -> *

Em outras palavras, Maybepega 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 Maybes - se tentar executar

[] :: [Maybe]

você receberá um erro. No entanto, você pode criar uma lista de Maybe Int, ou Maybe a. Isso Maybeocorre porque é uma função construtora de tipo, mas uma lista precisa conter valores de um tipo concreto. Maybe Inte Maybe asão tipos concretos (ou, se desejar, chamadas para digitar funções construtoras que retornam tipos concretos.)


2
No seu primeiro exemplo, RED GREEN e BLUE são construtores que não aceitam argumentos.
OllieB

3
A afirmação de que em 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 em data Tree a = Tip | Node a (Tree a) (Tree a)"existem dois construtores de dados, Tip e Node".
Frerich Raabe 13/08/13

1
@CMCDragonkai Você está absolutamente correto! Tipos são os "tipos de tipos". Uma abordagem comum para juntar os conceitos de tipos e valores é chamada de digitação dependente . Idris é uma linguagem tipicamente dependente, inspirada em Haskell. Com as extensões certas do GHC, você também pode se aproximar um pouco da digitação dependente em Haskell. (Algumas pessoas têm sido brincando que "a pesquisa Haskell é de cerca de descobrir como perto de tipos dependentes podemos obter sem ter tipos dependentes.")
kqr

1
@CMCDragonkai Na verdade, não é possível ter uma declaração de dados vazia no Haskell padrão. Mas existe uma extensão do GHC ( -XEmptyDataDecls) que permite fazer isso. Como, como você diz, não há valores com esse tipo, uma função f :: Int -> Zpode, 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 .
kqr

1
Realmente não é possível? Eu apenas tentei no GHC, e ele funcionou sem erros. Não precisei carregar nenhuma extensão do GHC, apenas baunilha do GHC. Eu poderia escrever :k Ze isso me deu uma estrela.
CMCDragonkai

42

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, Greene Blue).

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:

data Eg1 = One Int | Two String

Um Eg1valor é basicamente quer um inteiro ou uma string. Portanto, o conjunto de todos os Eg1valores 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 referem Eg1como um "tipo de soma". Por outro lado:

data Eg2 = Pair Int String

Cada Eg2valor consiste em ambos um inteiro e uma string. Portanto, o conjunto de todos os Eg2valores 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:

data Config = XML_Config {...} | JSON_Config {...}

(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.


4
ótimo! apenas uma coisa: "Eles podem ... ser construídos em praticamente qualquer idioma", diz a Wikipedia . :) Em, por exemplo, C / ++, isso é unions, com uma disciplina de tag. :)
Will Ness

5
Sim, mas toda vez que mencionei union, as pessoas me olham como "quem diabos usa isso ?" ;-)
MathematicalOrchid

1
Eu já vi muito unionusado na minha carreira em C. Por favor, não faça parecer desnecessário, porque esse não é o caso.
truthadjustr

26

Comece com o caso mais simples:

data Color = Blue | Green | Red

Isso define um "construtor de tipos" Colorque não aceita argumentos - e possui três "construtores de dados" Blue, Greene Red. Nenhum dos construtores de dados aceita argumentos. Isto significa que existem três tipo Color: Blue, Greene Red.

Um construtor de dados é usado quando você precisa criar um valor de algum tipo. Gostar:

myFavoriteColor :: Color
myFavoriteColor = Green

cria um valor myFavoriteColorusando o Greenconstrutor de dados - e myFavoriteColorserá do tipo, Colorpois 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:

isFavoriteColor :: Color -> Bool

Nesse caso, você está chamando o Colorconstrutor 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:

data Color = Blue Int | Green Int | Red Int

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 escrever

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

E, novamente, ele chama o Greenconstrutor de dados e eu recebo um valor do tipo Color.

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 Intnos construtores de dados, mas usar uma variável de tipo:

data Color a = Blue a | Green a | Red a

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 tipo a. Então você poderia ter

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

ou

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Observe como chamamos o Colorconstrutor 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.


Não sei se sou amigo da sua noção de construtor de dados nulo. Eu sei que é uma maneira comum de falar sobre constantes em Haskell, mas não foi provado incorreto algumas vezes?
kqr

@kqr: Um construtor de dados pode ser nulo, mas não é mais uma função. Uma função é algo que pega um argumento e gera um valor, ou seja, algo com ->na assinatura.
Frerich Raabe

Um valor pode apontar para vários tipos? Ou todo valor está associado a apenas um tipo e é isso?
CMCDragonkai

1
@jrg Existe alguma sobreposição, mas não é especificamente por causa dos construtores de tipo, mas por causa de variáveis ​​de tipo, por exemplo, o ain data 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) -> ausa uma tupla de dois valores (dos tipos ae b) 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.
Frerich Raabe

1
+1 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.
Jonas

5

Como outros apontaram, o polimorfismo não é tão útil aqui. Vejamos outro exemplo com o qual você provavelmente já está familiarizado:

Maybe a = Just a | Nothing

Este tipo possui dois construtores de dados. Nothingé um pouco chato, não contém dados úteis. Por outro lado, Justcontém um valor de a- qualquer que seja o tipo a. Vamos escrever uma função que use esse tipo, por exemplo, obter o cabeçalho de uma Intlista, se houver alguma (espero que você concorde que isso seja mais útil do que gerar um erro):

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

Portanto, neste caso, aé um Int, 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):

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

Por outro lado, você pode escrever funções que aceitam apenas um certo tipo de Maybe , por exemplo,

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

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 Stringnão é suficiente para identificar a empresa, mas ela precisa ter seu próprio tipo Company(que contém dados adicionais como país, endereço, contas bancárias etc.). Sua primeira implementação Carprecisaria ser alterada para ser usada em Companyvez do Stringprimeiro valor. Sua segunda implementação está ótima, você a usa como antes Car Company String Inte funcionaria como antes (é claro que as funções de acesso aos dados da empresa precisam ser alteradas).


Você pode usar construtores de tipo no contexto de dados de outra declaração de dados? Algo como 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.
CMCDragonkai

@CMCDragonkai Eu não acho que você possa fazer isso, e nem tenho certeza do que você deseja alcançar com isso. Você pode "quebrar" um tipo existente usando dataor newtype(por exemplo data Bright = Bright Color), ou você pode usar typepara definir um sinônimo (por exemplo type Bright = Color).
Landei

5

O segundo tem a noção de "polimorfismo".

O a b cpode ser de qualquer tipo. Por exemplo, apode ser um [String], bpode ser [Int] e cpode ser [Char].

Enquanto o primeiro tipo é fixo: empresa é a String, modelo é a Stringe 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.


1

É sobre tipos : no primeiro caso, você define os tipos String(por empresa e modelo) e Intpor ano. No segundo caso, você é mais genérico. a,, be cpodem 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 seu Colortipo.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.