Haskell Language
Estensioni di linguaggio GHC comuni
Ricerca…
Osservazioni
Queste estensioni di lingua sono in genere disponibili quando si utilizza il Glasgow Haskell Compiler (GHC) poiché non fanno parte del rapporto della lingua Haskell 2010 approvato. Per utilizzare queste estensioni, è necessario informare il compilatore utilizzando un flag o posizionare un programma LANGUAGE
prima della parola chiave module
in un file. La documentazione ufficiale è disponibile nella sezione 7 della guida utenti GCH.
Il formato del programma LANGUAGE
è {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. Questo è il letterale {-#
seguito da LANGUAGE
seguito da un elenco di estensioni separate da virgole e infine da #-}
chiusura. È possibile inserire più programmi LANGUAGE
in un unico file.
MultiParamTypeClasses
È un'estensione molto comune che consente classi di tipi con più parametri di tipo. Puoi pensare a MPTC come una relazione tra i tipi.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
L'ordine dei parametri è importante.
Gli MPTC possono a volte essere sostituiti con famiglie di tipi.
FlexibleInstances
Le istanze regolari richiedono:
All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Ciò significa che, ad esempio, mentre è possibile creare un'istanza per [a]
non è possibile creare un'istanza specifica per [Int]
.; FlexibleInstances
rilassa quello:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
OverloadedStrings
Normalmente, i valori letterali di stringa in Haskell hanno un tipo di String
(che è un alias di tipo per [Char]
). Mentre questo non è un problema per i programmi educativi più piccoli, le applicazioni del mondo reale richiedono spesso una memoria più efficiente come Text
o ByteString
.
OverloadedStrings
semplicemente cambia il tipo di letterali in
"test" :: Data.String.IsString a => a
Consentendo loro di passare direttamente alle funzioni che si aspettano tale tipo. Molte librerie implementano questa interfaccia per i loro tipi simili a stringhe, tra cui Data.Text e Data.ByteString, che offrono entrambi alcuni vantaggi in termini di tempo e spazio rispetto a [Char]
.
Ci sono anche alcuni usi unici di OverloadedStrings
come quelli della libreria semplice di Postgresql che consente di scrivere query SQL in virgolette doppie come una normale stringa, ma fornisce protezioni contro la concatenazione impropria, una famigerata fonte di attacchi SQL injection.
Per creare un'istanza della classe IsString
è necessario eseguire l' fromString
funzione fromString
. Esempio † :
data Foo = A | B | Other String deriving Show
instance IsString Foo where
fromString "A" = A
fromString "B" = B
fromString xs = Other xs
tests :: [ Foo ]
tests = [ "A", "B", "Testing" ]
† Questo esempio per gentile concessione di Lyndon Maydwell ( sordina
su GitHub) trovato qui .
TupleSections
Un'estensione sintattica che consente di applicare il costruttore tuple (che è un operatore) in una sezione:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-tuple
Funziona anche per le tuple con un'arità maggiore di due
(,2,) 1 3 == (1,2,3)
Mappatura
Questo può essere utile in altri luoghi in cui vengono utilizzate le sezioni:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
L'esempio precedente senza questa estensione sarebbe simile a questo:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Un'estensione che consente di utilizzare caratteri Unicode al posto di determinati operatori e nomi incorporati.
ASCII | Unicode | Utilizzare (s) |
---|---|---|
:: | ∷ | ha tipo |
-> | → | tipi di funzioni, lambda, case derivati, ecc. |
=> | ⇒ | vincoli di classe |
forall | ∀ | polimorfismo esplicito |
<- | ← | do notazione |
* | ★ | il tipo (o il tipo) di tipi (es. Int :: ★ ) |
>- | ⤚ | notazione proc per Arrows |
-< | ⤙ | notazione proc per Arrows |
>>- | ⤜ | notazione proc per Arrows |
-<< | ⤛ | notazione proc per Arrows |
Per esempio:
runST :: (forall s. ST s a) -> a
potrebbe diventare
runST ∷ (∀ s. ST s a) → a
Nota che l'esempio *
vs. ★
è leggermente diverso: poiché *
non è riservato, ★
funziona allo stesso modo di *
per la moltiplicazione, o qualsiasi altra funzione chiamata (*)
, e viceversa. Per esempio:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Haskell standard consente di scrivere valori letterali interi decimali (senza alcun prefisso), esadecimali (preceduti da 0x
o 0X
) e ottali (preceduti da 0o
o 0O
). L'estensione BinaryLiterals
aggiunge l'opzione di binario (preceduta da 0b
o 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Si tratta di un'estensione di sistema di tipo che consente tipi quantificati in modo esistenziale o, in altre parole, di variabili di tipo che vengono solo istanziate in fase di esecuzione † .
Un valore di tipo esistenziale è simile a un riferimento alla classe base astratta nei linguaggi OO: non si conosce il tipo esatto in contiene, ma è possibile vincolare la classe di tipi.
data S = forall a. Show a => S a
o in modo equivalente, con la sintassi GADT:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
I tipi esistenziali aprono la porta a cose come contenitori quasi eterogenei: come detto sopra, possono esserci vari tipi in un valore S
, ma tutti possono essere show
, quindi puoi anche fare
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Ora possiamo creare una collezione di tali oggetti:
ss = [S 5, S "test", S 3.0]
Il che ci consente anche di usare il comportamento polimorfico:
mapM_ print ss
Gli esistenziali possono essere molto potenti, ma si noti che in realtà non sono necessari molto spesso in Haskell. Nell'esempio sopra, tutto ciò che puoi effettivamente fare con l'istanza Show
è mostrare (duh!) I valori, ovvero creare una rappresentazione stringa. L'intero tipo S
quindi contiene esattamente quante informazioni come la stringa che si ottiene quando lo si mostra. Pertanto, di solito è meglio memorizzare semplicemente quella stringa immediatamente, specialmente dal momento che Haskell è pigro e quindi la stringa inizialmente sarà solo un thunk non valutato comunque.
D'altra parte, gli esistenziali causano alcuni problemi unici. Ad esempio, il modo in cui le informazioni sul tipo sono "nascoste" in un campo esistenziale. Se si esegue il pattern-match su un valore S
, si avrà il tipo contenuto in scope (più precisamente la sua istanza Show
), ma questa informazione non può mai sfuggire al suo scope, che diventa quindi un po 'una "società segreta": il compilatore non lascia che qualcosa sfugga all'ambito tranne valori il cui tipo è già noto dall'esterno. Questo può portare a strani errori come Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Confrontalo con il normale polimorfismo parametrico, che generalmente viene risolto in fase di compilazione (consentendo la cancellazione completa del tipo).
I tipi esistenziali sono diversi dai tipi Rank-N - queste estensioni sono, in parole povere, duali l'una all'altra: per utilizzare effettivamente i valori di un tipo esistenziale, è necessaria una funzione (possibilmente vincolata) polimorfica, come show
nell'esempio. Una funzione polimorfica è universalmente quantificata, cioè funziona per qualsiasi tipo in una determinata classe, mentre la quantificazione esistenziale significa che funziona per un tipo particolare che è a priori sconosciuto. Se hai una funzione polimorfica, è sufficiente, tuttavia per passare le funzioni polimorfiche come argomenti, hai bisogno di {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Un'estensione sintattica che consente di scrivere \case
al posto di \arg -> case arg of
.
Considera la seguente definizione di funzione:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Se vuoi evitare di ripetere il nome della funzione, potresti scrivere qualcosa come:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Usando l'estensione LambdaCase, puoi scriverlo come un'espressione di funzione, senza dover denominare l'argomento:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Immagina la seguente situazione:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Qui, vogliamo passare una funzione che converte un valore in una stringa, applicare quella funzione sia al parametro stringa che al parametro int e stamparli entrambi. Nella mia mente, non c'è motivo per cui questo dovrebbe fallire! Abbiamo una funzione che funziona su entrambi i tipi di parametri che stiamo passando.
Sfortunatamente, questo non scriverà check! GHC deduce a
tipo basato sulla sua prima occorrenza nel corpo della funzione. Cioè, non appena abbiamo colpito:
putStrLn (show' string)
GHC dedurrà quello show' :: String -> String
, poiché string
è una String
. Continuerà a saltare in aria mentre prova a show' int
.
RankNTypes
ti consente invece di scrivere la firma del tipo come segue, quantificando su tutte le funzioni che soddisfano il tipo di show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
Si tratta di rango 2 polimorfismo: stiamo affermando che la show'
funzione deve lavorare per tutti a
s all'interno della nostra funzione e l'attuazione precedente ora funziona.
L'estensione RankNTypes
consente l'annidamento arbitrario di tutti i forall ...
blocchi nelle firme dei tipi. In altre parole, consente il polimorfismo di grado N.
OverloadedLists
aggiunto in GHC 7.8 .
OverloadedLists, simile a OverloadedStrings , consente di convertire i letterali delle liste come segue:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Questo è utile quando si tratta di tipi come Set
, Vector
e Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
IsList
classe IsList
in GHC.Exts
deve essere utilizzata con questa estensione.
IsList
è dotato di una funzione tipo, Item
e tre funzioni, da fromList :: [Item l] -> l
, a toList :: l -> [Item l]
e fromListN :: Int -> [Item l] -> l
dove fromListN
è facoltativo. Le implementazioni tipiche sono:
instance IsList [a] where
type Item [a] = a
fromList = id
toList = id
instance (Ord a) => IsList (Set a) where
type Item (Set a) = a
fromList = Set.fromList
toList = Set.toList
Esempi tratti da OverloadedLists - GHC .
FunctionalDependencies
Se si dispone di una classe di tipo a più parametri con argomenti a, b, c e x, questa estensione consente di esprimere che il tipo x può essere identificato in modo univoco da a, b, e c:
class SomeClass a b c x | a b c -> x where ...
Quando si dichiara un'istanza di tale classe, verrà verificata rispetto a tutte le altre istanze per assicurarsi che la dipendenza funzionale sia valida, cioè non esiste un'altra istanza con lo stesso abc
ma esiste una x
differente.
È possibile specificare più dipendenze in un elenco separato da virgole:
class OtherClass a b c d | a b -> c d, a d -> b where ...
Ad esempio in MTL possiamo vedere:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Ora, se hai un valore di tipo MonadReader a ((->) Foo) => a
, il compilatore può dedurre che a ~ Foo
, poiché il secondo argomento determina completamente il primo e semplificherà il tipo di conseguenza.
La classe SomeClass
può essere pensata come una funzione degli argomenti abc
che risulta in x
. Tali classi possono essere utilizzate per eseguire calcoli nel sistema dei tipi.
GADTs
I tipi di dati algebrici convenzionali sono parametrici nelle loro variabili di tipo. Ad esempio, se definiamo un ADT come
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
con la speranza che questo eliminerà staticamente condizionali non ben digitati, questo non si comporterà come previsto poiché il tipo di IntLit :: Int -> Expr a
è IntLit :: Int -> Expr a
quantificato: per qualsiasi scelta di a
, produce un valore di tipo Expr a
. In particolare, per a ~ Bool
, abbiamo IntLit :: Int -> Expr Bool
, che ci consente di costruire qualcosa come If (IntLit 1) e1 e2
che è ciò che il tipo di costruttore di If
stava cercando di escludere.
I tipi di dati algebrici generalizzati ci consentono di controllare il tipo risultante di un costruttore di dati in modo che non siano semplicemente parametrici. Possiamo riscrivere il nostro tipo Expr
come GADT in questo modo:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Qui, il tipo del costruttore IntLit
è Int -> Expr Int
, e quindi IntLit 1 :: Expr Bool
non digiterà tipograficamente.
La corrispondenza del modello su un valore GADT causa il perfezionamento del tipo di termine restituito. Ad esempio, è possibile scrivere un valutatore per Expr a
questo modo:
crazyEval :: Expr a -> a
crazyEval (IntLit x) =
-- Here we can use `(+)` because x :: Int
x + 1
crazyEval (BoolLit b) =
-- Here we can use `not` because b :: Bool
not b
crazyEval (If b thn els) =
-- Because b :: Expr Bool, we can use `crazyEval b :: Bool`.
-- Also, because thn :: Expr a and els :: Expr a, we can pass either to
-- the recursive call to `crazyEval` and get an a back
crazyEval $ if crazyEval b then thn else els
Si noti che siamo in grado di usare (+)
nelle definizioni precedenti perché quando ad esempio IntLit x
corrisponde al modello, impariamo anche che a ~ Int
(e allo stesso modo per not
e if_then_else_
quando a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
ti consente di fare riferimento a tipi universalmente quantificati all'interno di una dichiarazione. Per essere più espliciti:
import Data.Monoid
foo :: forall a b c. (Monoid b, Monoid c) => (a, b, c) -> (b, c) -> (a, b, c)
foo (a, b, c) (b', c') = (a :: a, b'', c'')
where (b'', c'') = (b <> b', c <> c') :: (b, c)
La cosa importante è che siamo in grado di utilizzare a
, b
e c
per istruire il compilatore in sottoespressioni della dichiarazione (la tupla in where
clausola e la prima a
nel risultato finale). In pratica, ScopedTypeVariables
aiuta a scrivere funzioni complesse come una somma di parti, consentendo al programmatore di aggiungere firme di tipo a valori intermedi che non hanno tipi concreti.
PatternSynonyms
I sinonimi di pattern sono astrazioni di pattern simili a come le funzioni sono astrazioni di espressioni.
Per questo esempio, esaminiamo l'interfaccia che Data.Sequence
espone e vediamo come può essere migliorato con i sinonimi di pattern. Il tipo Seq
è un tipo di dati che, internamente, utilizza una rappresentazione complicata per ottenere una buona complessità asintotica per varie operazioni, in particolare O (1) (un) consing e (un) snocing.
Ma questa rappresentazione è ingombrante e alcuni dei suoi invarianti non possono essere espressi nel sistema di tipi di Haskell. Per questo Seq
, il tipo Seq
viene esposto agli utenti come un tipo astratto, insieme a funzioni di accesso e di costruzione che preservano invariabili, tra cui:
empty :: Seq a
(<|) :: a -> Seq a -> Seq a
data ViewL a = EmptyL | a :< (Seq a)
viewl :: Seq a -> ViewL a
(|>) :: Seq a -> a -> Seq a
data ViewR a = EmptyR | (Seq a) :> a
viewr :: Seq a -> ViewR a
Ma usare questa interfaccia può essere un po 'macchinoso:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Possiamo usare i modelli di visualizzazione per ripulirlo un po ':
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Usando l'estensione del linguaggio PatternSynonyms
, possiamo dare un'interfaccia ancora più carina, consentendo la corrispondenza dei pattern per far finta di avere una lista cons o snoc:
{-# LANGUAGE PatternSynonyms #-}
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
pattern Empty :: Seq a
pattern Empty <- (Seq.viewl -> Seq.EmptyL)
pattern (:<) :: a -> Seq a -> Seq a
pattern x :< xs <- (Seq.viewl -> x Seq.:< xs)
pattern (:>) :: Seq a -> a -> Seq a
pattern xs :> x <- (Seq.viewr -> xs Seq.:> x)
Questo ci permette di scrivere uncons
in uno stile molto naturale:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
Vedi RecordWildCards