Haskell Language
Typowe rozszerzenia języka GHC
Szukaj…
Uwagi
Te rozszerzenia językowe są zwykle dostępne podczas korzystania z kompilatora Glasgow Haskell (GHC), ponieważ nie są częścią zatwierdzonego raportu językowego Haskell 2010 . Aby skorzystać z tych rozszerzeń, należy poinformować kompilator za pomocą flagi lub umieścić program LANGUAGE
przed słowem kluczowym module
w pliku. Oficjalna dokumentacja znajduje się w sekcji 7 przewodnika dla użytkowników GCH.
Format programu LANGUAGE
to {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. To jest dosłowny {-#
po którym następuje LANGUAGE
po którym następuje lista oddzielonych przecinkami rozszerzeń, a na końcu #-}
. W jednym pliku może znajdować się wiele programów LANGUAGE
.
MultiParamTypeClasses
Jest to bardzo popularne rozszerzenie, które umożliwia klasy typów z wieloma parametrami typu. Możesz myśleć o MPTC jako związku między typami.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
Kolejność parametrów ma znaczenie.
MPTC można czasem zastąpić rodzinami typów.
Elastyczne substancje
Zwykłe instancje wymagają:
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.
Oznacza to, że na przykład podczas tworzenia instancji dla [a]
nie można utworzyć instancji dla konkretnie [Int]
.; FlexibleInstances
rozluźnia, że:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
OverloadedStrings
Zwykle literały łańcuchowe w Haskell mają typ String
(który jest aliasem typu dla [Char]
). Chociaż nie jest to problemem w przypadku mniejszych programów edukacyjnych, aplikacje w świecie rzeczywistym często wymagają bardziej wydajnego przechowywania, takiego jak Text
lub ByteString
.
OverloadedStrings
po prostu zmienia typ literałów na
"test" :: Data.String.IsString a => a
Umożliwiając bezpośrednie przekazywanie ich do funkcji oczekujących takiego typu. Wiele bibliotek implementuje ten interfejs dla swoich łańcuchowych typów, w tym Data.Text i Data.ByteString, które zapewniają pewne korzyści czasowe i przestrzenne w porównaniu z [Char]
.
Istnieją również pewne unikalne zastosowania OverloadedStrings
takie jak te z biblioteki Postgresql-simple, która umożliwia pisanie zapytań SQL w podwójnych cudzysłowach, takich jak normalny ciąg, ale zapewnia ochronę przed niewłaściwą konkatenacją, znanym źródłem ataków wstrzykiwania SQL.
Aby utworzyć instancję klasy IsString
, musisz fromString
funkcję fromString
. Przykład † :
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" ]
† Ten przykład dzięki uprzejmości Lyndon Maydwell ( sordina
na GitHub) znaleźć tutaj .
TupleSections
Rozszerzenie składniowe, które pozwala zastosować konstruktor krotki (który jest operatorem) w sposób przekrojowy:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
Krotki N.
Działa również dla krotek o arsenie większym niż dwa
(,2,) 1 3 == (1,2,3)
Mapowanie
Może to być przydatne w innych miejscach, w których używane są sekcje:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
Powyższy przykład bez tego rozszerzenia wyglądałby następująco:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Rozszerzenie, które pozwala używać znaków Unicode zamiast niektórych wbudowanych operatorów i nazw.
ASCII | Unicode | Użycie |
---|---|---|
:: | ∷ | ma typ |
-> | → | typy funkcji, lambdy, gałęzie case itp. |
=> | ⇒ | ograniczenia klasowe |
forall | ∀ | wyraźny polimorfizm |
<- | ← | do notacja |
* | ★ | rodzaj (lub rodzaj) typów (np. Int :: ★ ) |
>- | ⤚ | notacja proc dla Arrows |
-< | ⤙ | notacja proc dla Arrows |
>>- | ⤜ | notacja proc dla Arrows |
-<< | ⤛ | notacja proc dla Arrows |
Na przykład:
runST :: (forall s. ST s a) -> a
stanie się
runST ∷ (∀ s. ST s a) → a
Zauważ, że przykład *
vs. ★
jest nieco inny: ponieważ *
nie jest zarezerwowany, ★
działa również w ten sam sposób co *
dla mnożenia lub jakiejkolwiek innej funkcji o nazwie (*)
i odwrotnie. Na przykład:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Standardowy Haskell pozwala pisać literały całkowite w systemie dziesiętnym (bez żadnego prefiksu), szesnastkowym (poprzedzonym 0x
lub 0X
) i ósemkowym (poprzedzonym 0o
lub 0O
). Rozszerzenie BinaryLiterals
dodaje opcję binarną (poprzedzoną 0b
lub 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Jest to rozszerzenie systemu typów, które pozwala na typy, które są egzystencjalnie skwantyfikowane, lub innymi słowy, mają zmienne typu, które są tworzone tylko w czasie wykonywania † .
Wartość typu egzystencjalnego jest podobna do odwołania do klasy abstrakcyjnej w językach OO: nie wiesz, jaki dokładnie zawiera typ, ale możesz ograniczyć klasę typów.
data S = forall a. Show a => S a
lub równoważnie ze składnią GADT:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Egzystencjalne typy otwierają drzwi do takich rzeczy, jak prawie heterogeniczne pojemniki: jak powiedziano powyżej, w rzeczywistości mogą istnieć różne typy w wartości S
, ale wszystkie z nich mogą być show
n, dlatego możesz również zrobić
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Teraz możemy stworzyć kolekcję takich obiektów:
ss = [S 5, S "test", S 3.0]
Co pozwala nam również wykorzystać zachowanie polimorficzne:
mapM_ print ss
Egzystencje mogą być bardzo potężne, ale zauważ, że w Haskell nie są one często konieczne. W powyższym przykładzie wszystko, co możesz faktycznie zrobić z instancją Show
to show (duh!) Wartości, tj. Utworzenie reprezentacji ciągu. Cały typ S
zawiera zatem dokładnie tyle informacji, ile ciąg, który otrzymujesz, gdy go wyświetlasz. Dlatego zwykle lepiej jest po prostu od razu zapisać ten ciąg, zwłaszcza że Haskell jest leniwy, a zatem i tak początkowo będzie to tylko nieoceniony zgrzyt.
Z drugiej strony, egzystencjalne powodują pewne unikalne problemy. Na przykład sposób, w jaki informacja o typie jest „ukryta” w egzystencjalnym. Jeśli dopasujesz wzorzec dla wartości S
, będziesz mieć zawarty typ w zakresie (a dokładniej jego instancję Show
), ale ta informacja nigdy nie umknie jego zakresowi, który staje się więc trochę „tajnym stowarzyszeniem”: kompilatorem nie pozwala, aby cokolwiek wymknęło się poza zakres, z wyjątkiem wartości, których typ jest już znany z zewnątrz. Może to prowadzić do dziwnych błędów, takich jak Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Porównaj to ze zwykłym polimorfizmem parametrycznym, który jest zwykle rozwiązywany w czasie kompilacji (umożliwiając całkowite usunięcie typu).
Typy egzystencjalne różnią się od typów Rank-N - z grubsza mówiąc, rozszerzenia te są podwójnie względem siebie: aby faktycznie użyć wartości typu egzystencjalnego, potrzebujesz (prawdopodobnie ograniczonej) funkcji polimorficznej, takiej jak show
w przykładzie. Funkcja polimorficzna jest uniwersalnie skwantyfikowana, tzn. Działa dla dowolnego typu w danej klasie, podczas gdy kwantyfikacja egzystencjalna oznacza, że działa dla określonego typu, który jest z góry nieznany. Jeśli masz funkcję polimorficzną, to wystarczy, jednak aby przekazać funkcje polimorficzne jako argumenty, potrzebujesz {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Rozszerzenie składniowe, które pozwala na zapisanie \case
zamiast \arg -> case arg of
.
Rozważ następującą definicję funkcji:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Jeśli chcesz uniknąć powtarzania nazwy funkcji, możesz napisać coś takiego:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Korzystając z rozszerzenia LambdaCase, możesz napisać to jako wyrażenie funkcyjne, bez konieczności nazywania argumentu:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Wyobraź sobie następującą sytuację:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Tutaj chcemy przekazać funkcję, która konwertuje wartość na String, zastosować tę funkcję zarówno do parametru string, jak i parametru int i wydrukować je oba. Moim zdaniem nie ma powodu, by to się nie udawało! Mamy funkcję, która działa na oba typy przekazywanych parametrów.
Niestety nie będzie to sprawdzać typu! GHC wywodzi się a
typem w oparciu off z jego pierwszego wystąpienia w ciele funkcji. To znaczy, jak tylko trafimy:
putStrLn (show' string)
GHC wywnioskuje, że show' :: String -> String
, ponieważ string
jest String
. Zacznie wysadzać w powietrze podczas próby show' int
.
RankNTypes
pozwala zamiast tego napisać podpis typu w następujący sposób, kwantyfikując wszystkie funkcje spełniające typ show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
To jest polimorfizm Pozycja 2: jesteśmy twierdząc, że show'
funkcja musi działać dla wszystkich a
s w naszej funkcji, a poprzednia realizacja działa teraz.
Rozszerzenie RankNTypes
pozwala na dowolne zagnieżdżanie wszystkich forall ...
bloków w podpisach typów. Innymi słowy, pozwala na polimorfizm rangi N.
OverloadedLists
dodano w GHC 7.8 .
OverloadedLists, podobnie jak OverloadedStrings , pozwala na projektowanie literałów listy w następujący sposób:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Jest to przydatne, gdy mamy do czynienia z typami takimi jak Set
, Vector
i Map
.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
Klasa IsList
w GHC.Exts
jest przeznaczona do użytku z tym rozszerzeniem.
IsList
jest wyposażony w jedną funkcję typu, Item
i trzy funkcje, fromList :: [Item l] -> l
, toList :: l -> [Item l]
oraz fromListN :: Int -> [Item l] -> l
gdzie fromListN
jest opcjonalny. Typowe wdrożenia to:
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
Przykłady zaczerpnięte z OverloadedLists - GHC .
FunctionalDependencies
Jeśli masz wieloparametrową klasę typu z argumentami a, b, c i x, to rozszerzenie pozwala wyrazić, że typ x można jednoznacznie zidentyfikować na podstawie a, b i c:
class SomeClass a b c x | a b c -> x where ...
Podczas deklarowania instancji takiej klasy zostanie ona sprawdzona względem wszystkich innych instancji, aby upewnić się, że funkcjonalna zależność zachowuje, to znaczy, że nie istnieje żadna inna instancja z tym samym abc
ale innym x
.
Możesz określić wiele zależności na liście oddzielonej przecinkami:
class OtherClass a b c d | a b -> c d, a d -> b where ...
Na przykład w MTL możemy zobaczyć:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Teraz, jeśli masz wartość typu MonadReader a ((->) Foo) => a
, kompilator może wywnioskować, że a ~ Foo
, ponieważ drugi argument całkowicie określa pierwszy i odpowiednio uprości ten typ.
SomeClass
można traktować jako funkcję argumentów abc
które dają x
. Takie klasy mogą być używane do wykonywania obliczeń w systemie typów.
GADT
Konwencjonalne algebraiczne typy danych mają charakter parametryczny. Na przykład, jeśli zdefiniujemy ADT jak
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
z nadzieją, że statycznie wykluczy to IntLit :: Int -> Expr a
warunkowe, nie będzie to IntLit :: Int -> Expr a
zgodnie z oczekiwaniami, ponieważ typ IntLit :: Int -> Expr a
jest IntLit :: Int -> Expr a
kwantyfikowany: dla każdego wyboru a
, daje wartość typu Expr a
. W szczególności dla a ~ Bool
mamy IntLit :: Int -> Expr Bool
, co pozwala nam zbudować coś takiego jak If (IntLit 1) e1 e2
co jest typem konstruktora If
który próbował wykluczyć.
Uogólnione algebraiczne typy danych pozwalają nam kontrolować wynikowy typ konstruktora danych, dzięki czemu nie są one jedynie parametryczne. Możemy przepisać nasz typ Expr
jako GADT w następujący sposób:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
W tym przypadku typ konstruktora IntLit
to Int -> Expr Int
, a więc IntLit 1 :: Expr Bool
nie będzie sprawdzał typu.
Dopasowanie wzorca do wartości GADT powoduje uściślenie typu zwracanego terminu. Na przykład, możliwe jest napisanie ewaluatora dla Expr a
podobnego do tego:
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
Zauważ, że jesteśmy w stanie używać (+)
w powyższych definicjach, ponieważ gdy np. IntLit x
jest zgodny ze wzorcem, uczymy się również, że a ~ Int
(i podobnie dla not
i if_then_else_
kiedy a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
pozwala odwoływać się do uniwersalnie skwantyfikowanych typów w deklaracji. Mówiąc dokładniej:
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)
Ważne jest to, że możemy użyć a
, b
i c
aby poinstruować kompilator o podwyrażeniach deklaracji (krotka w klauzuli where
i pierwsza a
w wyniku końcowym). W praktyce ScopedTypeVariables
pomaga w pisaniu złożonych funkcji jako sumy części, pozwalając programiście dodawać podpisy typów do wartości pośrednich, które nie mają konkretnych typów.
PatternSynonimy
Synonimy wzorców to abstrakcje wzorców podobne do tego, jak funkcje są abstrakcjami wyrażeń.
W tym przykładzie przyjrzyjmy się interfejsowi Data.Sequence
i zobaczmy, jak można go poprawić za pomocą synonimów wzorców. Typ Seq
jest typem danych, który wewnętrznie wykorzystuje skomplikowaną reprezentację, aby osiągnąć dobrą asymptotyczną złożoność dla różnych operacji, w szczególności zarówno O (1) (nie) konsumującą i (nie) snocing.
Ale ta reprezentacja jest nieporęczna i niektórych jej niezmienników nie można wyrazić w systemie typów Haskella. Z tego powodu typ Seq
jest udostępniany użytkownikom jako typ abstrakcyjny, wraz z niezmienniczymi funkcjami akcesora i konstruktora, między innymi:
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
Ale korzystanie z tego interfejsu może być nieco kłopotliwe:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Możemy użyć wzorców widoku, aby go trochę oczyścić:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Korzystając z rozszerzenia języka PatternSynonyms
, możemy zapewnić jeszcze lepszy interfejs, umożliwiając dopasowywanie wzorców, aby udawać, że mamy listę wad lub list 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)
To pozwala nam pisać uncons
w bardzo naturalnym stylu:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
Zobacz RecordWildCards