Haskell Language
Vanliga GHC-språkförlängningar
Sök…
Anmärkningar
Dessa språkförlängningar är vanligtvis tillgängliga när du använder Glasgow Haskell Compiler (GHC) eftersom de inte ingår i den godkända Haskell 2010-språkrapporten . För att använda dessa tillägg måste man antingen informera kompilatorn med hjälp av en flagga eller placera ett LANGUAGE
program före module
nyckelord i en fil. Den officiella dokumentationen finns i avsnitt 7 i GCH-användarguiden.
Formatet för LANGUAGE
programmet är {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. Det är den bokstavliga {-#
följt av LANGUAGE
följt av en kommaseparerad lista med tillägg och slutligen slutningen #-}
. Flera LANGUAGE
program kan placeras i en fil.
MultiParamTypeClasses
Det är en mycket vanlig tillägg som tillåter typklasser med flera typparametrar. Du kan tänka på MPTC som en relation mellan typer.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
Parternas ordning är viktig.
MPTC kan ibland ersättas med typfamiljer.
FlexibleInstances
Regelbundna instanser kräver:
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.
Det betyder att du till exempel inte kan skapa en instans för specifikt [Int]
medan du kan skapa en instans för [a]
; FlexibleInstances
slappnar av att:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
OverloadedStrings
Normalt har strängbokstäver i Haskell en typ av String
(som är ett typalias för [Char]
). Även om detta inte är ett problem för mindre utbildningsprogram, kräver verkliga applikationer ofta mer effektiv lagring, t.ex. Text
eller ByteString
.
OverloadedStrings
ändrar helt enkelt bokstäverna till
"test" :: Data.String.IsString a => a
Tillåter dem att direkt överföras till funktioner som förväntar sig en sådan typ. Många bibliotek implementerar detta gränssnitt för sina strängliknande typer inklusive Data.Text och Data.ByteString som både ger vissa tids- och utrymme fördelar jämfört med [Char]
.
Det finns också några unika användningar av OverloadedStrings
som de från det Postgresql-enkla biblioteket som gör det möjligt att skriva SQL-frågor i dubbla citat som en vanlig sträng, men ger skydd mot felaktig sammanlänkning, en beryktad källa för SQL-injektionsattacker.
För att skapa en instans av klassen IsString
måste du implementera fromString
funktionen. Exempel † :
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" ]
† Detta exempel med tillstånd av Lyndon Maydwell ( sordina
på GitHub) hittades här .
TupleSections
En syntaktisk förlängning som gör det möjligt att applicera tupelkonstruktorn (som är en operatör) på ett avsnitt:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-tupler
Det fungerar också för tupler med mer än två arity
(,2,) 1 3 == (1,2,3)
kartläggning
Detta kan vara användbart på andra platser där avsnitt används:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
Exemplet ovan utan denna tillägg skulle se ut så här:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Ett tillägg som låter dig använda Unicode-tecken istället för vissa inbyggda operatörer och namn.
ASCII | Unicode | Användning (ar) |
---|---|---|
:: | ∷ | har typ |
-> | → | funktionstyper, lambdas, case grenar, etc. |
=> | ⇒ | klass begränsningar |
forall | ∀ | uttrycklig polymorfism |
<- | ← | do notation |
* | ★ | typ (eller typ) av typer (t.ex. Int :: ★ ) |
>- | ⤚ | proc notation för Arrows |
-< | ⤙ | proc notation för Arrows |
>>- | ⤜ | proc notation för Arrows |
-<< | ⤛ | proc notation för Arrows |
Till exempel:
runST :: (forall s. ST s a) -> a
skulle bli
runST ∷ (∀ s. ST s a) → a
Observera att exemplet *
vs. ★
är något annorlunda: eftersom *
inte är reserverat, fungerar ★
också på samma sätt som *
för multiplikation, eller någon annan funktion som heter (*)
, och vice versa. Till exempel:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Standard Haskell låter dig skriva heltalslitteraler i decimal (utan något prefix), hexadecimal (föregås av 0x
eller 0X
) och oktal (föregås av 0o
eller 0O
). BinaryLiterals
tillägget lägger till alternativet för binär (föregås av 0b
eller 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Det här är ett typsystemförlängning som tillåter typer som är existentiellt kvantifierade, eller med andra ord har typvariabler som bara instanseras vid körning † .
Värdet existentiell typ liknar en abstrakt-base-klass referens i OO språk: du vet inte den exakta typen i innehåller, men du kan begränsa klassen typer.
data S = forall a. Show a => S a
eller motsvarande, med GADT-syntax:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Existentiella typer öppnar dörren för saker som nästan heterogena behållare: som sagt ovan kan det faktiskt finnas olika typer i ett S
värde, men alla kan vara show
n, därmed kan du också göra
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Nu kan vi skapa en samling av sådana objekt:
ss = [S 5, S "test", S 3.0]
Som också tillåter oss att använda det polymorfa beteendet:
mapM_ print ss
Existentials kan vara mycket kraftfulla, men observera att de faktiskt inte är nödvändiga så ofta i Haskell. I exemplet ovan, allt du faktiskt kan göra med Show
instansen är att visa (duh!) Värdena, dvs. skapa en strängrepresentation. Hela S
typen innehåller därför exakt lika mycket information som strängen du får när du visar den. Därför är det vanligtvis bättre att helt enkelt lagra den strängen direkt, speciellt eftersom Haskell är lat och därför kommer strängen till en början bara vara en obevärderad thunk ändå.
Å andra sidan orsakar existensiella problem unika problem. Till exempel hur typinformationen är "dold" i en existensiell. Om du mönstermatchar på ett S
värde, har du den innehöll typen i omfattning (mer exakt dess Show
instans), men denna information kan aldrig undgå sin omfattning, som därför blir lite av ett "hemligt samhälle": kompilatorn låter inget undkomma omfattningen utom värden vars typ redan är känd från utsidan. Detta kan leda till konstiga fel som inte Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Kontrastera detta med vanlig parametrisk polymorfism, som generellt löses vid sammanställningstiden (tillåter radering av full typ).
Existentiella typer skiljer sig från Rank-N-typer - dessa tillägg är grovt sett dubbla mot varandra: för att faktiskt använda värden av en existentiell typ behöver du en (eventuellt begränsad-) polymorf funktion, som show
i exemplet. En polymorf funktion är universellt kvantifierad, det vill säga den fungerar för alla typer i en given klass, medan existentiell kvantifiering innebär att den fungerar för någon viss typ som är priori okänd. Om du har en polymorfisk funktion är det tillräckligt, men för att klara polymorfa funktioner som sådana, behöver du {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
En syntaktisk förlängning som låter dig skriva \case
i stället för \arg -> case arg of
.
Tänk på följande funktionsdefinition:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Om du vill undvika att upprepa funktionsnamnet kan du skriva något som:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Med hjälp av LambdaCase-förlängningen kan du skriva det som ett funktionsuttryck utan att behöva namnge argumentet:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Föreställ dig följande situation:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Här vill vi överföra en funktion som konverterar ett värde till en sträng, tillämpa den funktionen på både en strängparameter och en int-parameter och skriva ut båda. Enligt min mening finns det ingen anledning att detta skulle misslyckas! Vi har en funktion som fungerar på båda typerna av parametrar som vi skickar in.
Tyvärr skriver det inte check! GHC tillhandahåller a
typ baserad på den första händelsen i funktionskroppen. Det är, så snart vi träffade:
putStrLn (show' string)
GHC kommer att sluta sig till att show' :: String -> String
, eftersom string
är en String
. Det fortsätter att spränga medan du försöker show' int
.
RankNTypes
låter dig istället skriva RankNTypes
sätt, kvantifiera över alla funktioner som uppfyller show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
Detta är rank 2 polymorfism: Vi hävdar att show'
funktionen måste fungera för alla a
s inom vår funktion, och den tidigare genomförande fungerar nu.
RankNTypes
tillåter godtycklig häckning av forall ...
block i typsignaturer. Med andra ord tillåter det rang N-polymorfism.
OverloadedLists
tillsatt i GHC 7.8 .
Överbelastade listor, liknar OverloadedStrings , gör det möjligt att desugareda listlitteraler enligt följande:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Detta är praktiskt när man hanterar typer som Set
, Vector
och Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
IsList
klassen i GHC.Exts
är avsedd att användas med den här utvidgningen.
IsList
är utrustad med en typfunktion, Item
och tre funktioner, fromList :: [Item l] -> l
, toList :: l -> [Item l]
och fromListN :: Int -> [Item l] -> l
där fromListN
är valfritt. Typiska implementeringar är:
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
Exempel tagna från OverloadedLists - GHC .
FunctionalDependencies
Om du har en typklass med flera parametrar med argumenten a, b, c och x låter det här tillägget uttrycka att typen x kan identifieras unikt från a, b och c:
class SomeClass a b c x | a b c -> x where ...
När man förklarar en instans av en sådan klass kommer det att kontrolleras mot alla andra instanser för att se till att det funktionella beroendet rymmer, det vill säga ingen annan instans med samma abc
men olika x
finns.
Du kan ange flera beroenden i en kommaseparerad lista:
class OtherClass a b c d | a b -> c d, a d -> b where ...
I MTL kan vi till exempel se:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Om du har ett värde av typen MonadReader a ((->) Foo) => a
kan kompilatorn dra slutsatsen att a ~ Foo
, eftersom det andra argumentet bestämmer det första och kommer att förenkla typen därefter.
SomeClass
kan ses som en funktion av argumenten abc
som resulterar i x
. Sådana klasser kan användas för att göra beräkningar i typsystemet.
GADTs
Konventionella algebraiska datatyper är parametriska i sina typvariabler. Om vi till exempel definierar en ADT som
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
med hopp om att denna statiskt kommer att utesluta icke-väl skrivit villkors, kommer detta inte beter sig som förväntat eftersom den typ av IntLit :: Int -> Expr a
är universially kvantifieras: för något val av a
, ger det ett värde av typen Expr a
Speciellt för a ~ Bool
har vi IntLit :: Int -> Expr Bool
, vilket gör att vi kan konstruera något som If (IntLit 1) e1 e2
vilket är vad typen av konstruktören If
försökte utesluta.
Generaliserade algebraiska datatyper tillåter oss att kontrollera den resulterande typen av en datakonstruktör så att de inte bara är parametriska. Vi kan skriva om vår Expr
typ som en GADT så här:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Här är typen av konstruktören IntLit
Int -> Expr Int
, och så kommer IntLit 1 :: Expr Bool
inte att kontrollera.
Mönstermatchning på ett GADT-värde orsakar förfining av typen av returnerad term. Till exempel är det möjligt att skriva en utvärderare för Expr a
som denna:
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
Observera att vi kan använda (+)
i ovanstående definitioner eftersom när t.ex. IntLit x
är mönstermatchat, lär vi oss också att a ~ Int
(och likaså för not
och if_then_else_
när a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
du hänvisa till universellt kvantifierade typer i en deklaration. För att vara mer tydlig:
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)
Det viktiga är att vi kan använda a
, b
och c
att instruera kompilatorn i underuttryck av deklarationen (tupeln i where
klausulen och den första a
i det slutliga resultatet). I praktiken hjälper ScopedTypeVariables
att skriva komplexa funktioner som en summa av delar, vilket gör att programmeraren kan lägga till typsignaturer till mellanvärden som inte har konkreta typer.
PatternSynonyms
Mönstersynonymer är abstraktioner av mönster som liknar hur funktioner är abstraktioner av uttryck.
För detta exempel, låt oss titta på gränssnittet Data.Sequence
exponerar, och låt oss se hur det kan förbättras med mönstersynonymer. Seq
typen är en datatyp som internt använder en komplicerad representation för att uppnå god asymptotisk komplexitet för olika operationer, särskilt både O (1) (un) consing och (un) snocing.
Men denna representation är olämplig och några av dess invarianter kan inte uttryckas i Haskells typsystem. På grund av detta exponeras Seq
typen för användare som en abstrakt typ, tillsammans med invariantbevarande accessor- och konstruktionsfunktioner, bland dem:
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
Men att använda detta gränssnitt kan vara lite besvärligt:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Vi kan använda visningsmönster för att rensa upp det något:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Med hjälp av PatternSynonyms
kan vi ge ett ännu trevligare gränssnitt genom att låta mönstermatchning låtsas att vi har en nackdel eller en snoc-lista:
{-# 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)
Detta gör att vi kan skriva uncons
i en mycket naturlig stil:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing