Qui, "valori", "tipi" e "tipi" hanno significati formali, quindi considerando il loro uso inglese comune o analogie con la classificazione delle automobili ti porterà solo finora.
La mia risposta riguarda specificamente i significati formali di questi termini nel contesto di Haskell; questi significati si basano (sebbene non siano realmente identici a) i significati usati nella "teoria dei tipi" matematica / CS. Quindi, questa non sarà un'ottima risposta "informatica", ma dovrebbe servire come una risposta Haskell piuttosto buona.
In Haskell (e in altre lingue), risulta utile assegnare un tipo a un'espressione di programma che descriva la classe di valori che può avere l'espressione. Presumo qui che tu abbia visto abbastanza esempi per capire perché sarebbe utile sapere che nell'espressione sqrt (a**2 + b**2)
, le variabili a
e b
saranno sempre valori di tipo Double
e non, diciamo, String
e Bool
rispettivamente. Fondamentalmente, avere tipi ci aiuta a scrivere espressioni / programmi che funzioneranno correttamente su una vasta gamma di valori .
Ora, qualcosa che potresti non aver capito è che i tipi di Haskell, come quelli che compaiono nelle firme dei tipi:
fmap :: Functor f => (a -> b) -> f a -> f b
sono in realtà scritti da loro stessi in una sublingua Haskell a livello di tipo. Il testo del programma Functor f => (a -> b) -> f a -> f b
è - letteralmente - un'espressione di tipo scritta in questo sublanguage. La sublanguage comprende operatori (ad esempio, ->
è un diritto operatore infisso associativa in questa lingua), variabili (ad esempio f
, a
e b
), e "applicazione" di un'espressione tipo ad un altro (ad esempio, f a
viene f
applicato a a
).
Ho già parlato di come sia stato utile in molti linguaggi assegnare tipi alle espressioni di programma per descrivere le classi di valori di espressione? Bene, in questa sublanguage a livello di tipo, le espressioni valutano i tipi (anziché i valori ) e finisce per essere utile assegnare i tipi alle espressioni di tipo per descrivere le classi di tipi che sono autorizzati a rappresentare. Fondamentalmente, avere tipi ci aiuta a scrivere espressioni di tipo che funzioneranno correttamente su una vasta gamma di tipi .
Quindi, i valori sono ai tipi come i tipi ai tipi , ei tipi ci aiutano a scrivere programmi di livello- valore mentre i tipi ci aiutano a scrivere programmi di livello- tipo .
Che aspetto hanno questi tipi ? Bene, considera la firma del tipo:
id :: a -> a
Se l'espressione del tipo a -> a
deve essere valida, che tipo di tipi dovremmo consentire a
alla variabile ? Bene, le espressioni di tipo:
Int -> Int
Bool -> Bool
sembrano validi, quindi i tipi Int
e Bool
sono ovviamente del tipo giusto . Ma tipi ancora più complicati come:
[Double] -> [Double]
Maybe [(Double,Int)] -> Maybe [(Double,Int)]
sembra valido. Infatti, dal momento che dovremmo essere in grado di richiamare id
funzioni, anche:
(a -> a) -> (a -> a)
sembra a posto. Così, Int
, Bool
, [Double]
, Maybe [(Double,Int)]
, e a -> a
sembrano tutti i tipi del diritto tipo .
In altre parole, sembra che ci sia un solo tipo , chiamiamolo *
un jolly Unix, e ogni tipo ha lo stesso tipo *
, fine della storia.
Destra?
Bene, non proprio. Si scopre che Maybe
, da sola, è valida un'espressione di tipo quanto Maybe Int
(allo stesso modo sqrt
, tutta da sola, è altrettanto valida un'espressione di valore sqrt 25
). Tuttavia , l'espressione di tipo seguente non è valida:
Maybe -> Maybe
Perché, sebbene Maybe
sia un'espressione di tipo, non rappresenta il tipo di tipo che può avere valori. Quindi, è così che dovremmo definire *
: è il tipo di tipi che hanno valori; include tipi "completi" come Double
o Maybe [(Double,Int)]
ma esclude tipi incompleti e senza valore come Either String
. Per semplicità, chiamerò questi tipi completi di tipi *
"tipi concreti", sebbene questa terminologia non sia universale, e "tipi concreti" potrebbe significare qualcosa di molto diverso, per esempio, un programmatore C ++.
Ora, l'espressione tipo a -> a
, a condizione che il tipo a
ha tipo *
(il tipo di tipi concreti), il risultato dell'espressione tipo a -> a
avrà anche avere tipo *
(ad esempio, il tipo di tipi concreti).
Quindi, che tipo di tipo è Maybe
? Bene, Maybe
può essere applicato a un tipo di calcestruzzo per produrre un altro tipo di calcestruzzo. Quindi, Maybe
sembra un po 'come una funzione a livello di tipo che accetta un tipo di tipo *
e restituisce un tipo di tipo *
. Se avessimo una funzione a livello di valore che assumesse un valore di tipo Int
e restituisse un valore di tipo Int
, gli daremmo una firma di tipoInt -> Int
, quindi per analogia dovremmo dare Maybe
una firma di tipo* -> *
. GHCi concorda:
> :kind Maybe
Maybe :: * -> *
Tornando a:
fmap :: Functor f => (a -> b) -> f a -> f b
In questo tipo di firma, la variabile f
ha tipo * -> *
e variabili a
e b
ha tipo *
; l'operatore incorporato ->
ha tipo * -> * -> *
(prende un tipo di tipo *
a sinistra e uno a destra e restituisce un tipo anche di tipo *
). Da questo e le regole di tipo deduzione, si può dedurre che a -> b
è un tipo valido con il genere *
, f a
e f b
sono anche tipi validi con il genere *
, e (a -> b) -> f a -> f b
è valido tipo di genere *
.
In altre parole, il compilatore può "controllare il tipo" nell'espressione del tipo (a -> b) -> f a -> f b
per verificare che sia valido per le variabili del tipo giusto allo stesso modo in cui "controlla il tipo" sqrt (a**2 + b**2)
per verificare che sia valido per le variabili del tipo giusto.
La ragione per usare termini separati per "tipi" rispetto a "tipi" (cioè, non parlare dei "tipi di tipi") è principalmente solo per evitare confusione. I tipi sopra sembrano molto diversi dai tipi e, almeno all'inizio, sembrano comportarsi in modo abbastanza diverso. (Ad esempio, ci vuole del tempo per avvolgere la testa intorno all'idea che ogni tipo "normale" ha lo stesso tipo *
e il tipo di non lo a -> b
è .)*
* -> *
Alcuni di questi sono anche storici. Con l'evoluzione di GHC Haskell, le distinzioni tra valori, tipi e tipi hanno iniziato a confondersi. In questi giorni, i valori possono essere "promossi" in tipi e tipi e tipi sono davvero la stessa cosa. Quindi, nella moderna Haskell, i valori hanno entrambi tipi e tipi ARE (quasi), e i tipi di tipi sono solo più tipi.
@ user21820 ha chiesto una spiegazione aggiuntiva di "tipi e tipi sono davvero la stessa cosa". Per essere un po 'più chiari, nel moderno GHC Haskell (dalla versione 8.0.1, credo), i tipi e i tipi sono trattati in modo uniforme nella maggior parte del codice del compilatore. Il compilatore compie alcuni sforzi nei messaggi di errore per distinguere tra "tipi" e "tipi", a seconda che si stia lamentando rispettivamente del tipo di un valore o del tipo di un tipo.
Inoltre, se non sono abilitate estensioni, sono facilmente distinguibili nella lingua di superficie. Ad esempio, i tipi (di valori) hanno una rappresentazione nella sintassi (ad esempio, nelle firme dei tipi), ma i tipi (di tipi) sono - penso - completamente impliciti e non c'è sintassi esplicita in cui compaiono.
Ma se si attivano le estensioni appropriate, la distinzione tra tipi e tipi scompare in gran parte. Per esempio:
{-# LANGUAGE GADTs, TypeInType #-}
data Foo where
Bar :: Bool -> * -> Foo
Qui, Bar
è (sia un valore che) un tipo. Come tipo, il suo tipo è Bool -> * -> Foo
, che è una funzione a livello di tipo che accetta un tipo di tipo Bool
(che è un tipo, ma anche un tipo) e un tipo di tipo *
e produce un tipo di tipo Foo
. Così:
type MyBar = Bar True Int
verifiche del tipo corretto.
Come spiega @AndrejBauer nella sua risposta, l'incapacità di distinguere tra tipi e tipi non è sicura - avere un tipo / tipo il *
cui tipo / tipo è esso stesso (come nel caso della moderna Haskell) porta a paradossi. Tuttavia, il sistema di tipi di Haskell è già pieno di paradossi a causa della non terminazione, quindi non è considerato un grosso problema.