Tipo Haskell vs Data Constructor


124

Sto imparando Haskell da learnyouahaskell.com . Ho problemi a comprendere i costruttori di tipi e i costruttori di dati. Ad esempio, non capisco davvero la differenza tra questo:

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

e questo:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Capisco che il primo sta semplicemente usando un costruttore ( Car) per costruire dati di tipo Car. Non capisco davvero il secondo.

Inoltre, come fanno i tipi di dati definiti in questo modo:

data Color = Blue | Green | Red

rientrare in tutto questo?

Da quello che ho capito, il terzo esempio ( Color) è un tipo che può essere in tre stati: Blue, Greeno Red. Ma questo è in conflitto con il modo in cui ho capito i primi due esempi: è che il tipo Carpuò essere solo in uno stato Car, che può richiedere vari parametri per essere compilato? In tal caso, come si inserisce il secondo esempio?

In sostanza, sto cercando una spiegazione che unisca i tre esempi / costrutti di codice precedenti.


18
L'esempio di Car potrebbe creare un po 'di confusione perché Carè sia un costruttore di tipi (sul lato sinistro di =) che un costruttore di dati (sul lato destro). Nel primo esempio, il Carcostruttore del tipo non accetta argomenti, nel secondo esempio ne richiede tre. In entrambi gli esempi, il Carcostruttore di dati accetta tre argomenti (ma i tipi di questi argomenti sono in un caso fissi e nell'altro parametrizzati).
Simon Shine

il primo consiste semplicemente nell'usare un costruttore di dati ( Car :: String -> String -> Int -> Car) per creare dati di tipo Car. il secondo utilizza semplicemente un costruttore di dati ( Car :: a -> b -> c -> Car a b c) per creare dati di tipo Car a b c.
Will Ness

Risposte:


228

In una datadichiarazione, un costruttore di tipo è l'elemento a sinistra del segno di uguale. I costruttori di dati sono le cose a destra del segno di uguale. Si utilizzano costruttori di tipi in cui è previsto un tipo e si utilizzano costruttori di dati in cui è previsto un valore.

Costruttori di dati

Per rendere le cose semplici, possiamo iniziare con un esempio di un tipo che rappresenta un colore.

data Colour = Red | Green | Blue

Qui abbiamo tre costruttori di dati. Colourè un tipo ed Greenè un costruttore che contiene un valore di tipo Colour. Allo stesso modo, Rede Bluesono entrambi costruttori che costruiscono valori di tipo Colour. Potremmo immaginare di aromatizzarlo però!

data Colour = RGB Int Int Int

Abbiamo ancora solo il tipo Colour, ma RGBnon è un valore: è una funzione che prende tre Ints e restituisce un valore! RGBha il tipo

RGB :: Int -> Int -> Int -> Colour

RGBè un costruttore di dati che è una funzione che prende alcuni valori come argomenti e quindi li usa per costruire un nuovo valore. Se hai eseguito una programmazione orientata agli oggetti, dovresti riconoscerlo. In OOP, i costruttori accettano anche alcuni valori come argomenti e restituiscono un nuovo valore!

In questo caso, se applichiamo RGBa tre valori, otteniamo un valore di colore!

Prelude> RGB 12 92 27
#0c5c1b

Abbiamo costruito un valore di tipo Colourapplicando il costruttore di dati. Un costruttore di dati contiene un valore come farebbe una variabile oppure prende altri valori come argomento e crea un nuovo valore . Se hai programmato in precedenza, questo concetto non dovrebbe essere molto strano per te.

Intervallo

Se desideri costruire un albero binario per memorizzare Stringi messaggi di posta elettronica , potresti immaginare di fare qualcosa di simile

data SBTree = Leaf String
            | Branch String SBTree SBTree

Quello che vediamo qui è un tipo SBTreeche contiene due costruttori di dati. In altre parole, ci sono due funzioni (vale a dire Leafe Branch) che costruiranno valori del SBTreetipo. Se non hai familiarità con il funzionamento degli alberi binari, resta lì. In realtà non è necessario sapere come funzionano gli alberi binari, solo che questo memorizza Stringi messaggi di posta elettronica in qualche modo.

Vediamo anche che entrambi i costruttori di dati accettano un Stringargomento: questa è la stringa che memorizzeranno nell'albero.

Ma! E se volessimo anche essere in grado di memorizzare Bool, dovremmo creare un nuovo albero binario. Potrebbe assomigliare a questo:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Tipo costruttori

Entrambi SBTreee BBTreesono costruttori di tipi. Ma c'è un problema evidente. Vedi quanto sono simili? Questo è un segno che vuoi davvero un parametro da qualche parte.

Quindi possiamo farlo:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)

Ora introduciamo una variabile di tipo a come parametro nel costruttore del tipo. In questa dichiarazione, BTreeè diventata una funzione. Accetta un tipo come argomento e restituisce un nuovo tipo .

E 'importante prendere in considerazione la differenza tra un tipo concreto (esempi includono Int, [Char]e Maybe Bool), che è un tipo che può essere assegnato a un valore nel programma, ed una funzione di tipo costruttore di cui avete bisogno per alimentare un tipo di essere in grado di essere assegnato a un valore. Un valore non può mai essere di tipo "lista", perché deve essere una "lista di qualcosa ". Nello stesso spirito, un valore non può mai essere di tipo "albero binario", perché deve essere un "albero binario che memorizza qualcosa ".

Se passiamo, diciamo, Boolcome argomento a BTree, restituisce il tipo BTree Bool, che è un albero binario che memorizza Bools. Sostituisci ogni occorrenza della variabile di tipo acon il tipo Boole puoi vedere di persona come è vero.

Se vuoi, puoi visualizzare BTreecome una funzione con il tipo

BTree :: * -> *

I tipi sono un po 'come i tipi: *indica un tipo concreto, quindi diciamo che BTreeva da un tipo concreto a un tipo concreto.

Avvolgendo

Fai un passo indietro qui e prendi nota delle somiglianze.

  • Un costruttore di dati è una "funzione" che accetta 0 o più valori e restituisce un nuovo valore.

  • Un costruttore di tipi è una "funzione" che accetta 0 o più tipi e ti restituisce un nuovo tipo.

I costruttori di dati con parametri sono interessanti se vogliamo leggere variazioni nei nostri valori: inseriamo queste variazioni nei parametri e lasciamo che il tizio che crea il valore decida quali argomenti inseriranno. Allo stesso modo, i costruttori di tipi con parametri sono interessanti se vogliamo leggere variazioni nelle nostre tipologie! Mettiamo queste variazioni come parametri e lasciamo che il ragazzo che crea il tipo decida quali argomenti inserire.

Un caso di studio

Come tratto iniziale qui, possiamo considerare il Maybe atipo. La sua definizione è

data Maybe a = Nothing
             | Just a

Ecco Maybeun costruttore di tipi che restituisce un tipo concreto. Justè un costruttore di dati che restituisce un valore. Nothingè un costruttore di dati che contiene un valore. Se guardiamo il tipo di Just, lo vediamo

Just :: a -> Maybe a

In altre parole, Justprende un valore di tipo ae restituisce un valore di tipo Maybe a. Se guardiamo il tipo di Maybe, lo vediamo

Maybe :: * -> *

In altre parole, Maybeprende un tipo concreto e restituisce un tipo concreto.

Di nuovo! La differenza tra un tipo concreto e una funzione di costruzione del tipo. Non è possibile creare un elenco di messaggi di posta Maybeelettronica se si tenta di eseguire

[] :: [Maybe]

riceverai un errore. È tuttavia possibile creare un elenco di Maybe Int, o Maybe a. Questo perché Maybeè una funzione di costruzione del tipo, ma un elenco deve contenere valori di un tipo concreto. Maybe Inte Maybe asono tipi concreti (o, se si desidera, chiamate a funzioni di costruzione di tipi che restituiscono tipi concreti.)


2
Nel tuo primo esempio, sia ROSSO VERDE che BLU sono costruttori che non accettano argomenti.
OllieB

3
L'affermazione che in data Colour = Red | Green | Blue"non abbiamo alcun costruttore" è chiaramente sbagliata. I costruttori di tipi e i costruttori di dati non hanno bisogno di prendere argomenti, vedere ad esempio haskell.org/haskellwiki/Constructor che sottolinea che in data Tree a = Tip | Node a (Tree a) (Tree a)"ci sono due costruttori di dati, Tip e Node".
Frerich Raabe

1
@CMCDragonkai Hai assolutamente ragione! I tipi sono i "tipi di tipi". Un approccio comune per unire i concetti di tipi e valori è chiamato tipizzazione dipendente . Idris è un linguaggio di battitura dipendente ispirato a Haskell. Con le giuste estensioni GHC, puoi anche avvicinarti un po 'alla digitazione dipendente in Haskell. (Alcune persone hanno scherzato dicendo che "la ricerca Haskell riguarda il capire quanto ci si possa avvicinare ai tipi dipendenti senza avere tipi dipendenti.")
kqr

1
@CMCDragonkai In realtà non è possibile avere una dichiarazione di dati vuota nello standard Haskell. Ma c'è un'estensione GHC ( -XEmptyDataDecls) che ti permette di farlo. Poiché, come dici tu, non ci sono valori con quel tipo, una funzione f :: Int -> Zad esempio potrebbe non tornare mai (perché cosa restituirebbe?) Possono tuttavia essere utili quando vuoi i tipi ma non ti preoccupi dei valori .
kqr

1
Davvero non è possibile? Ho appena provato in GHC e l'ho eseguito senza errori. Non ho dovuto caricare alcuna estensione GHC, solo GHC vaniglia. Potrei quindi scrivere :k Ze mi ha dato una stella.
CMCDragonkai

42

Haskell ha tipi di dati algebrici , che pochissimi altri linguaggi hanno. Questo è forse ciò che ti confonde.

In altre lingue, di solito puoi creare un "record", "struct" o simile, che ha una serie di campi denominati che contengono vari tipi di dati diversi. Si può anche a volte fare un "conteggio", che ha un (piccolo) insieme di valori fissi possibili (ad esempio, la vostra Red, Greene Blue).

In Haskell, puoi combinare entrambi contemporaneamente. Strano, ma vero!

Perché si chiama "algebrico"? Ebbene, i nerd parlano di "tipi di somma" e "tipi di prodotto". Per esempio:

data Eg1 = One Int | Two String

Un Eg1valore è fondamentalmente sia un numero intero o una stringa. Quindi l'insieme di tutti i Eg1valori possibili è la "somma" dell'insieme di tutti i possibili valori interi e di tutti i possibili valori stringa. Pertanto, i nerd si riferiscono a Eg1come un "tipo di somma". D'altro canto:

data Eg2 = Pair Int String

Ogni Eg2valore è composto sia da un numero intero che da una stringa. Quindi l'insieme di tutti i Eg2valori possibili è il prodotto cartesiano dell'insieme di tutti i numeri interi e dell'insieme di tutte le stringhe. I due set vengono "moltiplicati" insieme, quindi questo è un "tipo di prodotto".

I tipi algebrici di Haskell sono tipi di somma di tipi di prodotto . Si danno a un costruttore più campi per creare un tipo di prodotto e si hanno più costruttori per fare una somma (di prodotti).

Come esempio del motivo per cui ciò potrebbe essere utile, supponi di avere qualcosa che restituisce dati come XML o JSON e richiede un record di configurazione, ma ovviamente le impostazioni di configurazione per XML e per JSON sono completamente diverse. Quindi potresti fare qualcosa del genere:

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

(Con alcuni campi adatti, ovviamente.) Non puoi fare cose del genere nei normali linguaggi di programmazione, motivo per cui la maggior parte delle persone non ci è abituata.


4
grande! solo una cosa, "Possono ... essere costruiti in quasi tutte le lingue", dice Wikipedia . :) Ad esempio in C / ++, è unions, con una disciplina dei tag. :)
Will Ness

5
Sì, ma ogni volta che ho citato union, la gente mi guarda come "chi diavolo usa mai che ??" ;-)
MathematicalOrchid

1
Ne ho visti molti unionusati nella mia carriera in C. Per favore, non farlo sembrare inutile perché non è così.
truthadjustr

26

Inizia con il caso più semplice:

data Color = Blue | Green | Red

Questo definisce un "costruttore di tipi" Colorche non accetta argomenti - e ha tre "costruttori di dati" Blue, Greene Red. Nessuno dei costruttori di dati accetta argomenti. Questo significa che ci sono tre di tipo Color: Blue, Greene Red.

Un costruttore di dati viene utilizzato quando è necessario creare un valore di qualche tipo. Piace:

myFavoriteColor :: Color
myFavoriteColor = Green

crea un valore myFavoriteColorutilizzando il Greencostruttore di dati e myFavoriteColorsarà di tipo Colorpoiché questo è il tipo di valori prodotti dal costruttore di dati.

Un costruttore di tipi viene utilizzato quando è necessario creare un tipo di qualche tipo. Questo di solito è il caso quando si scrivono firme:

isFavoriteColor :: Color -> Bool

In questo caso, stai chiamando il Colorcostruttore del tipo (che non accetta argomenti).

Ancora con me?

Ora, immagina di voler creare non solo valori rosso / verde / blu, ma anche di specificare una "intensità". Ad esempio, un valore compreso tra 0 e 256. Puoi farlo aggiungendo un argomento a ciascuno dei costruttori di dati, in modo da ottenere:

data Color = Blue Int | Green Int | Red Int

Ora, ciascuno dei tre costruttori di dati accetta un argomento di tipo Int. Il type constructor ( Color) non accetta ancora alcun argomento. Quindi, il mio colore preferito è un verde scuro, potrei scrivere

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

E ancora, chiama il Greencostruttore di dati e ottengo un valore di tipo Color.

Immagina di non voler dettare il modo in cui le persone esprimono l'intensità di un colore. Alcuni potrebbero volere un valore numerico come abbiamo appena fatto. Altri possono andare bene con solo un booleano che indica "brillante" o "non così brillante". La soluzione a questo è non codificare Intin modo rigido nei costruttori di dati ma piuttosto utilizzare una variabile di tipo:

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

Ora, il nostro costruttore di tipi accetta un argomento (un altro tipo che chiamiamo semplicemente a!) E tutti i costruttori di dati prenderanno un argomento (un valore!) Di quel tipo a. Quindi potresti avere

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

o

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Nota come chiamiamo il Colorcostruttore del tipo con un argomento (un altro tipo) per ottenere il tipo "effettivo" che verrà restituito dai costruttori di dati. Questo tocca il concetto di tipi di cui potresti voler leggere davanti a una tazza di caffè o due.

Ora abbiamo capito cosa sono i costruttori di dati e i costruttori di tipi e come i costruttori di dati possono prendere altri valori come argomenti e i costruttori di tipi possono prendere altri tipi come argomenti. HTH.


Non sono sicuro di essere amico della tua idea di un costruttore di dati nulli. So che è un modo comune per parlare di costanti in Haskell, ma non è stato dimostrato errato un bel po 'di volte?
kqr

@kqr: un costruttore di dati può essere nullo, ma non è più una funzione. Una funzione è qualcosa che accetta un argomento e restituisce un valore, cioè qualcosa con ->nella firma.
Frerich Raabe

Un valore può indicare più tipi? O ogni valore è associato a un solo tipo e basta?
CMCDragonkai

1
@jrg C'è qualche sovrapposizione, ma non è specificamente a causa dei costruttori di tipo ma a causa delle variabili di tipo, ad esempio ain data Color a = Red a. aè un segnaposto per un tipo arbitrario. Puoi avere lo stesso nelle funzioni semplici, ad esempio, una funzione di tipo (a, b) -> aprende una tupla di due valori (di tipi ae b) e restituisce il primo valore. È una funzione "generica" ​​in quanto non detta il tipo degli elementi della tupla, ma specifica solo che la funzione restituisce un valore dello stesso tipo del primo elemento della 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.Questo è molto utile.
Jonas

5

Come altri hanno sottolineato, il polimorfismo non è così terribile utile qui. Diamo un'occhiata a un altro esempio con cui probabilmente hai già familiarità:

Maybe a = Just a | Nothing

Questo tipo ha due costruttori di dati. Nothingè un po 'noioso, non contiene dati utili. D'altra parte Justcontiene un valore di a- qualunque tipo apossa avere. Scriviamo una funzione che utilizza questo tipo, ad esempio ottenere l'inizio di una Intlista, se ce n'è (spero che tu sia d'accordo che questo sia più utile che lanciare un errore):

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

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

Quindi in questo caso aè un Int, ma funzionerebbe anche per qualsiasi altro tipo. Infatti puoi far funzionare la nostra funzione per ogni tipo di lista (anche senza cambiarne l'implementazione):

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

D'altra parte puoi scrivere funzioni che accettano solo un certo tipo di Maybe, ad es

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

Per farla breve, con il polimorfismo dai al tuo tipo la flessibilità di lavorare con valori di altri tipi diversi.

Nel tuo esempio, potresti decidere a un certo punto che Stringnon è sufficiente per identificare l'azienda, ma deve avere il suo tipo Company(che contiene dati aggiuntivi come paese, indirizzo, account precedenti, ecc.). La tua prima implementazione di Cardovrebbe cambiare da usare Companyinvece che Stringper il suo primo valore. La tua seconda implementazione va benissimo, la usi come Car Company String Inte funzionerebbe come prima (ovviamente le funzioni che accedono ai dati aziendali devono essere modificate).


Potete usare i costruttori di tipo nel contesto dei dati di un'altra dichiarazione di dati? Qualcosa di simile data Color = Blue ; data Bright = Color? L'ho provato in ghci e sembra che il colore nel costruttore del tipo non abbia nulla a che fare con il costruttore dei dati del colore nella definizione di Bright. Ci sono solo 2 costruttori di colore, uno che è Dati e l'altro è Tipo.
CMCDragonkai

@CMCDragonkai Non penso che tu possa farlo, e non sono nemmeno sicuro di cosa vuoi ottenere con questo. È possibile "racchiudere" un tipo esistente utilizzando datao newtype(ad esempio data Bright = Bright Color), oppure è possibile utilizzare typeper definire un sinonimo (ad esempio type Bright = Color).
Landei

5

Il secondo contiene la nozione di "polimorfismo".

La a b cpuò essere di qualsiasi tipo. Ad esempio, apuò essere [String], bpuò essere [Int] e cpuò essere [Char].

Mentre il primo tipo è fisso: la società è una String, il modello è una Stringe l'anno è Int.

L'esempio di Car potrebbe non mostrare l'importanza dell'uso del polimorfismo. Ma immagina che i tuoi dati siano del tipo elenco. Un elenco può contenere String, Char, Int ...In queste situazioni, sarà necessario il secondo modo per definire i dati.

Per quanto riguarda il terzo modo, non credo che debba adattarsi al tipo precedente. È solo un altro modo per definire i dati in Haskell.

Questa è la mia modesta opinione come principiante anch'io.

Btw: Assicurati di allenare bene il tuo cervello e di sentirti a tuo agio. È la chiave per capire Monad in seguito.


1

Riguarda i tipi : nel primo caso, imposta i tipi String(per azienda e modello) e Intper anno. Nel secondo caso, sei più generico. a, be cpossono essere gli stessi tipi del primo esempio o qualcosa di completamente diverso. Ad esempio, può essere utile fornire l'anno come stringa anziché come numero intero. E se vuoi, puoi anche usare il tuo Colortipo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.