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 !



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow