Haskell Language
Tipo di applicazione
Ricerca…
introduzione
TypeApplications
è un'alternativa alle annotazioni di tipo quando il compilatore fatica a dedurre i tipi per una determinata espressione.
Questa serie di esempi spiegherà lo scopo dell'estensione TypeApplications
e come usarlo
Non dimenticare di abilitare l'estensione posizionando {-# LANGUAGE TypeApplications #-}
nella parte superiore del file sorgente.
Evitare annotazioni di tipo
Usiamo annotazioni di tipo per evitare ambiguità. Le applicazioni di tipo possono essere utilizzate per lo stesso scopo. Per esempio
x :: Num a => a
x = 5
main :: IO ()
main = print x
Questo codice ha un errore di ambiguità. Sappiamo che a
ha un'istanza Num
e per stamparla sappiamo che ha bisogno di un'istanza Show
. Questo potrebbe funzionare se a
fosse, per esempio, un Int
, quindi per correggere l'errore possiamo aggiungere un'annotazione di tipo
main = print (x :: Int)
Un'altra soluzione che utilizza le applicazioni di tipo sarebbe simile a questa
main = print @Int x
Per capire cosa significa questo dobbiamo guardare alla firma del tipo di print
.
print :: Show a => a -> IO ()
La funzione accetta un parametro di tipo a
, ma un altro modo di guardarlo è che in realtà richiede due parametri. Il primo è un parametro di tipo , il secondo è un valore il cui tipo è il primo parametro.
La principale differenza tra i parametri di valore e i parametri di tipo è che questi ultimi sono implicitamente forniti alle funzioni quando li chiamiamo. Chi li fornisce? L'algoritmo di inferenza del tipo! Cosa facciamo di TypeApplications
è dare quei parametri di tipo esplicitamente. Ciò è particolarmente utile quando l'inferenza del tipo non può determinare il tipo corretto.
Quindi, per suddividere l'esempio precedente
print :: Show a => a -> IO ()
print @Int :: Int -> IO ()
print @Int x :: IO ()
Digitare applicazioni in altre lingue
Se hai familiarità con linguaggi come Java, C # o C ++ e il concetto di generici / modelli, questo confronto potrebbe essere utile per te.
Diciamo che abbiamo una funzione generica in C #
public static T DoNothing<T>(T in) { return in; }
Per chiamare questa funzione con un float
possiamo fare DoNothing(5.0f)
o se vogliamo essere espliciti possiamo dire DoNothing<float>(5.0f)
. Quella parte all'interno delle parentesi angolari è l'applicazione del tipo.
In Haskell è lo stesso, tranne che i parametri di tipo non sono solo impliciti nei siti di chiamata ma anche nei siti di definizione.
doNothing :: a -> a
doNothing x = x
Questo può anche essere reso esplicito usando le estensioni ScopedTypeVariables
, Rank2Types
o RankNTypes
come questa.
doNothing :: forall a. a -> a
doNothing x = x
Quindi sul sito di chiamata possiamo scrivere nuovamente doNothing 5.0
o doNothing @Float 5.0
Ordine dei parametri
Il problema con gli argomenti di tipo impliciti diventa ovvio una volta che ne abbiamo più di uno. In quale ordine entrano?
const :: a -> b -> a
Scrivere const @Int
significa a
è uguale a Int
, o è b
? Nel caso in cui dichiariamo esplicitamente i parametri di tipo usando forall
come const :: forall a b. a -> b -> a
quindi l'ordine è come scritto: a
, quindi b
.
Se non lo facciamo, l'ordine delle variabili va da sinistra a destra. La prima variabile da menzionare è il primo parametro di tipo, il secondo è il secondo parametro di tipo e così via.
Cosa succede se vogliamo specificare la seconda variabile di tipo, ma non la prima? Possiamo usare un carattere jolly per la prima variabile come questa
const @_ @Int
Il tipo di questa espressione è
const @_ @Int :: a -> Int -> a
Interazione con tipi ambigui
Supponiamo che tu stia introducendo una classe di tipi che hanno una dimensione in byte.
class SizeOf a where
sizeOf :: a -> Int
Il problema è che la dimensione dovrebbe essere costante per ogni valore di quel tipo. In realtà non vogliamo che la funzione sizeOf
dipenda da a
, ma solo dal suo tipo.
Senza applicazioni di tipo, la soluzione migliore era il tipo Proxy
definito in questo modo
data Proxy a = Proxy
Lo scopo di questo tipo è di portare informazioni sul tipo, ma senza informazioni sul valore. Quindi la nostra classe potrebbe assomigliare a questo
class SizeOf a where
sizeOf :: Proxy a -> Int
Ora ti starai chiedendo, perché non abbandonare del tutto il primo argomento? Il tipo della nostra funzione sarebbe quindi solo sizeOf :: Int
o, per essere più precisi perché è un metodo di una classe, sizeOf :: SizeOf a => Int
o essere ancora più esplicitamente sizeOf :: forall a. SizeOf a => Int
.
Il problema è l'inferenza di tipo. Se scrivo sizeOf
da qualche parte, l'algoritmo di inferenza sa solo che mi aspetto un Int
. Non ha idea di quale tipo voglio sostituire per a
. Per questo {-# LANGUAGE AllowAmbiguousTypes #-}
, la definizione viene rifiutata dal compilatore a meno che non sia abilitato l'estensione {-# LANGUAGE AllowAmbiguousTypes #-}
. In quel caso la definizione viene compilata, ma non può essere utilizzata ovunque senza un errore di ambiguità.
Fortunatamente, l'introduzione di applicazioni di tipo salva la giornata! Ora possiamo scrivere sizeOf @Int
, dicendo esplicitamente che a
è Int
. Le applicazioni di tipo ci consentono di fornire un parametro di tipo, anche se non appare nei parametri effettivi della funzione !