Haskell Language
Algemene GHC-taalextensies
Zoeken…
Opmerkingen
Deze taalextensies zijn meestal beschikbaar wanneer u de Glasgow Haskell Compiler (GHC) gebruikt, omdat ze geen deel uitmaken van het goedgekeurde taalrapport Haskell 2010 . Om deze extensies te gebruiken, moet men ofwel de compiler te informeren met behulp van een vlag of plaats een LANGUAGE
programa voor de module
zoekwoord in een bestand. De officiële documentatie is te vinden in sectie 7 van de GCH-gebruikershandleiding.
Het formaat van het LANGUAGE
programma is {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. Dat is de letterlijke {-#
gevolgd door LANGUAGE
gevolgd door een door komma's gescheiden lijst met extensies, en ten slotte de afsluitende #-}
. Meerdere LANGUAGE
programma's kunnen in één bestand worden geplaatst.
MultiParamTypeClasses
Het is een veel voorkomende extensie die typeklassen met meerdere typeparameters mogelijk maakt. Je kunt MPTC beschouwen als een relatie tussen typen.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
De volgorde van parameters is belangrijk.
MPTC's kunnen soms worden vervangen door type families.
FlexibleInstances
Regelmatige instanties vereisen:
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.
Dat betekent dat, terwijl je bijvoorbeeld een instantie kunt maken voor [a]
je geen instantie kunt maken voor specifiek [Int]
.; FlexibleInstances
ontspant dat:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
OverloadedStrings
Normaal hebben stringliterals in Haskell een type String
(wat een type-alias is voor [Char]
). Hoewel dit geen probleem is voor kleinere educatieve programma's, vereisen real-world applicaties vaak een efficiëntere opslag, zoals Text
of ByteString
.
OverloadedStrings
verandert eenvoudig het type literals in
"test" :: Data.String.IsString a => a
Zodat ze direct kunnen worden doorgegeven aan functies die een dergelijk type verwachten. Veel bibliotheken implementeren deze interface voor hun string-achtige typen, waaronder Data.Text en Data.ByteString die beide bepaalde tijd- en ruimtevoordelen bieden ten opzichte van [Char]
.
Er zijn ook enkele unieke toepassingen van OverloadedStrings
zoals die uit de eenvoudige bibliotheek van Postgresql waarmee SQL-query's in dubbele aanhalingstekens kunnen worden geschreven, zoals een normale string, maar biedt bescherming tegen onjuiste aaneenschakeling, een beruchte bron van SQL-injectieaanvallen.
Om een instantie van de klasse IsString
, moet u de fromString
implementeren. Voorbeeld † :
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" ]
† Dit voorbeeld met dank aan Lyndon Maydwell ( sordina
op GitHub) hier gevonden.
TupleSections
Een syntactische extensie waarmee de tuple-constructor (een operator) kan worden toegepast op een sectie-manier:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-tupels
Het werkt ook voor tupels met een arity groter dan twee
(,2,) 1 3 == (1,2,3)
In kaart brengen
Dit kan handig zijn op andere plaatsen waar secties worden gebruikt:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
Het bovenstaande voorbeeld zonder deze extensie zou er als volgt uitzien:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Een extensie waarmee u Unicode-tekens kunt gebruiken in plaats van bepaalde ingebouwde operatoren en namen.
ASCII | Unicode | Toepassingen) |
---|---|---|
:: | ∷ | heeft type |
-> | → | functietypen, lambda, case takken, etc. |
=> | ⇒ | klassenbeperkingen |
forall | ∀ | expliciet polymorfisme |
<- | ← | do notatie |
* | ★ | het type (of soort) van typen (bijv. Int :: ★ ) |
>- | ⤚ | proc notatie voor Arrows |
-< | ⤙ | proc notatie voor Arrows |
>>- | ⤜ | proc notatie voor Arrows |
-<< | ⤛ | proc notatie voor Arrows |
Bijvoorbeeld:
runST :: (forall s. ST s a) -> a
zou worden
runST ∷ (∀ s. ST s a) → a
Merk op dat het voorbeeld van *
versus ★
enigszins verschilt: omdat *
niet gereserveerd is, werkt ★
ook op dezelfde manier als *
voor vermenigvuldiging, of een andere functie met de naam (*)
, en omgekeerd. Bijvoorbeeld:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Met Standaard Haskell kunt u gehele letterlijke getallen schrijven in decimaal (zonder voorvoegsel), hexadecimaal (voorafgegaan door 0x
of 0X
) en octaal (voorafgegaan door 0o
of 0O
). De extensie BinaryLiterals
voegt de optie binair toe (voorafgegaan door 0b
of 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Dit is een type systeemuitbreiding waarmee types kunnen worden gebruikt die existentieel zijn gekwantificeerd, of met andere woorden, typevariabelen hebben die alleen tijdens runtime worden geïnstantieerd † .
Een waarde van het existentiële type is vergelijkbaar met een verwijzing naar een abstract-basisklasse in OO-talen: u weet niet het exacte type in, maar u kunt de klasse van typen beperken.
data S = forall a. Show a => S a
of gelijkwaardig, met GADT-syntaxis:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Existentiële types openen de deur naar dingen zoals bijna-heterogene containers: zoals hierboven gezegd, er kunnen eigenlijk verschillende typen in een S
waarde zijn, maar ze kunnen allemaal worden show
, dus je kunt ook
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Nu kunnen we een verzameling van dergelijke objecten maken:
ss = [S 5, S "test", S 3.0]
Dat stelt ons ook in staat om het polymorfe gedrag te gebruiken:
mapM_ print ss
Existentials kunnen erg krachtig zijn, maar merk op dat ze eigenlijk niet vaak nodig zijn in Haskell. In het bovenstaande voorbeeld is het enige dat u met de instantie Show
kunt doen, de waarden weergeven (duh!), Dwz een stringvoorstelling maken. Het hele S
type bevat daarom exact evenveel informatie als de tekenreeks die u krijgt bij het weergeven. Daarom is het meestal beter om die string gewoon meteen op te slaan, vooral omdat Haskell lui is en daarom de string in eerste instantie in elk geval alleen een niet-beoordeelde thunk zal zijn.
Aan de andere kant veroorzaken existentialiteiten enkele unieke problemen. Bijvoorbeeld, de manier waarop de type-informatie is 'verborgen' in een existentieel. Als u een patroon-match maakt op een S
waarde, hebt u het ingesloten type in bereik (meer precies, de Show
instantie), maar deze informatie kan nooit aan het bereik ontsnappen, wat daarom een beetje een "geheime genootschap" wordt: de compiler laat niets aan het bereik ontsnappen, behalve waarden waarvan het type al van buiten bekend is. Dit kan leiden tot vreemde fouten zoals Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Vergelijk dit met het gewone parametrische polymorfisme, dat over het algemeen tijdens het compileren wordt opgelost (waardoor het type volledig kan worden gewist).
Bestaande typen verschillen van Rank-N-typen - deze uitbreidingen zijn grofweg dubbel van elkaar: om waarden van een existentieel type daadwerkelijk te gebruiken, hebt u een (mogelijk beperkte) polymorfe functie nodig, zoals in het voorbeeld wordt show
. Een polymorfe functie is universeel gekwantificeerd, dat wil zeggen dat het voor elk type in een bepaalde klasse werkt, terwijl existentiële kwantificatie betekent dat het voor een bepaald type werkt dat a priori onbekend is. Als je een polymorfe functie hebt, is dat voldoende, maar om polymorfe functies als argumenten door te geven, heb je {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Een syntactische extensie waarmee u \case
kunt schrijven in plaats van \arg -> case arg of
.
Overweeg de volgende functiedefinitie:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Als u wilt voorkomen dat de functienaam wordt herhaald, kunt u zoiets schrijven als:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Met de extensie LambdaCase kunt u dat als een functie-uitdrukking schrijven, zonder het argument te hoeven noemen:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Stel je de volgende situatie voor:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Hier willen we een functie doorgeven die een waarde omzet in een String, die functie toepassen op zowel een stringparameter als de parameter int en ze beide afdrukken. Naar mijn mening is er geen reden waarom dit zou mislukken! We hebben een functie die werkt op beide typen parameters die we doorgeven.
Helaas zal dit geen controle typen! GHC leidt het a
soort gebaseerd van de eerste vermelding in de functie lichaam. Dat wil zeggen, zodra we slaan:
putStrLn (show' string)
GHC zal die show' :: String -> String
afleiden show' :: String -> String
, omdat string
een String
. Het zal opblazen terwijl het probeert het te show' int
.
RankNTypes
kunt u in plaats daarvan de typeaanduiding als volgt schrijven en alle functies kwantificeren die voldoen aan het type van de show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
Dit is rang 2 polymorfisme: We zijn te beweren dat de show'
functie moet werk voor iedereen a
s binnen onze functie, en de vorige uitvoering werkt nu.
De extensie RankNTypes
maakt willekeurig nesten van forall ...
blokken in type handtekeningen mogelijk. Met andere woorden, het maakt polymorfisme van rang N mogelijk.
OverloadedLists
toegevoegd in GHC 7.8 .
OverloadedLists, vergelijkbaar met OverloadedStrings , laat toe om lijstliteralen als volgt te ontcijferen:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Dit is handig bij het omgaan met typen zoals Set
, Vector
en Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
IsList
klasse in GHC.Exts
is bedoeld voor gebruik met deze extensie.
IsList
is uitgerust met één type functie, Item
en drie functies, fromList :: [Item l] -> l
, toList :: l -> [Item l]
en fromListN :: Int -> [Item l] -> l
waar fromListN
is optioneel. Typische implementaties zijn:
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
Voorbeelden uit OverloadedLists - GHC .
FunctionalDependencies
Als u een type-klasse met meerdere parameters heeft met argumenten a, b, c en x, kunt u met deze extensie uitdrukken dat het type x uniek kan worden geïdentificeerd uit a, b en c:
class SomeClass a b c x | a b c -> x where ...
Wanneer een instantie van een dergelijke klasse wordt gedeclareerd, wordt deze vergeleken met alle andere instanties om ervoor te zorgen dat de functionele afhankelijkheid geldt, dat wil zeggen dat er geen andere instantie met dezelfde abc
maar een andere x
bestaat.
U kunt meerdere afhankelijkheden opgeven in een door komma's gescheiden lijst:
class OtherClass a b c d | a b -> c d, a d -> b where ...
In MTL kunnen we bijvoorbeeld zien:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Als u nu een waarde van het type MonadReader a ((->) Foo) => a
, kan de compiler die a ~ Foo
afleiden, omdat het tweede argument de eerste volledig bepaalt en het type dienovereenkomstig zal vereenvoudigen.
De klasse SomeClass
kan worden beschouwd als een functie van de argumenten abc
die resulteert in x
. Dergelijke klassen kunnen worden gebruikt om berekeningen in het typesysteem uit te voeren.
GADTs
Conventionele algebraïsche gegevenstypen zijn parametrisch in hun typevariabelen. Als we bijvoorbeeld een ADT zoals definiëren
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
in de hoop dat dit statisch niet-goed getypeerde voorwaardes zal uitsluiten, zal dit zich niet gedragen zoals verwacht, aangezien het type IntLit :: Int -> Expr a
wordt gekwantificeerd: voor elke keuze van a
, produceert het een waarde van type Expr a
. In het bijzonder hebben we voor a ~ Bool
IntLit :: Int -> Expr Bool
, waardoor we zoiets als If (IntLit 1) e1 e2
construeren, wat het type van de If
constructor probeerde uit te sluiten.
Gegeneraliseerde algebraïsche gegevenstypen stellen ons in staat om het resulterende type van een gegevensconstructor te besturen, zodat deze niet alleen parametrisch zijn. We kunnen ons Expr
type als een GADT als volgt herschrijven:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Hier is het type van de constructor IntLit
Int -> Expr Int
, en dus zal IntLit 1 :: Expr Bool
niet tikken.
Patroonovereenkomst op een GADT-waarde veroorzaakt verfijning van het type geretourneerde term. Het is bijvoorbeeld mogelijk om een evaluator voor Expr a
te schrijven zoals:
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
Merk op dat we in de bovenstaande definities (+)
kunnen gebruiken, omdat wanneer bijvoorbeeld IntLit x
overeenkomt met een patroon, we ook leren dat a ~ Int
(en ook voor not
en if_then_else_
wanneer a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
kunt u verwijzen naar universeel gekwantificeerde typen binnen een aangifte. Om meer expliciet te zijn:
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)
Het belangrijkste is dat we kunnen gebruiken a
, b
en c
te instrueren de compiler in subexpressies van de verklaring (de tupel in de where
clausule en de eerste a
in het uiteindelijke resultaat). In de praktijk helpt ScopedTypeVariables
bij het schrijven van complexe functies als een som van delen, waardoor de programmeur typeaanduidingen kan toevoegen aan tussenliggende waarden die geen concrete typen hebben.
PatternSynonyms
Patroonsynoniemen zijn abstracties van patronen vergelijkbaar met hoe functies abstracties van uitdrukkingen zijn.
Laten we voor dit voorbeeld kijken naar de interface Data.Sequence
onthult, en laten we kijken hoe het kan worden verbeterd met patroonsynoniemen. Het Seq
type is een gegevenstype dat intern een gecompliceerde weergave gebruikt om een goede asymptotische complexiteit te bereiken voor verschillende bewerkingen, met name zowel O (1) (on) consing en (un) snocing.
Maar deze weergave is log en sommige van zijn invarianten kunnen niet worden uitgedrukt in het type systeem van Haskell. Vanwege dit wordt het Seq
type aan gebruikers als een abstract type blootgesteld, samen met invariant-conserverende accessor- en constructorfuncties, waaronder:
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
Maar het gebruik van deze interface kan een beetje omslachtig zijn:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
We kunnen weergavepatronen gebruiken om het enigszins op te ruimen:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Met behulp van de PatternSynonyms
kunnen we een nog mooiere interface bieden door patroonvergelijking te laten doen alsof we een tegens- of een snoc-lijst hebben:
{-# 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)
Dit stelt ons in staat om uncons
in een zeer natuurlijke stijl te schrijven:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
Zie RecordWildCards