Haskell Language
Общие расширения языка GHC
Поиск…
замечания
Эти языковые расширения обычно доступны при использовании компилятора Glasgow Haskell (GHC), поскольку они не являются частью утвержденного отчета о языке Haskell 2010 . Чтобы использовать эти расширения, нужно либо проинформировать компилятор, используя флаг, либо поместить программу LANGUAGE
перед ключевым словом module
в файле. Официальную документацию можно найти в разделе 7 руководства пользователя GCH.
Формат программы LANGUAGE
- {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. Это буквальный {-#
за которым следует LANGUAGE
за которым следует список расширений, разделенных запятыми, и, наконец, закрытие #-}
. Несколько программ LANGUAGE
могут быть помещены в один файл.
MultiParamTypeClasses
Это очень распространенное расширение, которое позволяет использовать классы типов с несколькими параметрами типа. Вы можете думать о MPTC как о связи между типами.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
Порядок параметров имеет значение.
Иногда MPTC можно заменить семействами типов.
FlexibleInstances
Регулярные экземпляры требуют:
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.
Это означает, что, например, пока вы можете создать экземпляр для [a]
вы не можете создать экземпляр специально для [Int]
.; FlexibleInstances
расслабляет:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
OverloadedStrings
Обычно строковые литералы в Haskell имеют тип String
(который является псевдонимом типа для [Char]
). Хотя это не проблема для небольших образовательных программ, в реальных приложениях часто требуется более эффективное хранилище, например Text
или ByteString
.
OverloadedStrings
просто меняет тип литералов на
"test" :: Data.String.IsString a => a
Предоставление им прямого доступа к функциям, ожидающим такого типа. Многие библиотеки реализуют этот интерфейс для своих строковых типов, включая Data.Text и Data.ByteString, которые обеспечивают определенные преимущества по времени и пространству по сравнению с [Char]
.
Есть также некоторые уникальные применения OverloadedStrings
подобные тем, что из простой библиотеки Postgresql, которая позволяет SQL-запросам записываться в двойных кавычках, таких как обычная строка, но обеспечивает защиту от неправильной конкатенации, пресловутого источника атак SQL-инъекций.
Чтобы создать экземпляр класса IsString
вам необходимо внедрить функцию fromString
. Пример †:
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" ]
† Этот пример любезно Линдона Maydwell ( sordina
на GitHub) нашел здесь .
TupleSections
Синтаксическое расширение, которое позволяет применять конструктор кортежа (который является оператором) в виде раздела:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-кортежи
Он также работает для кортежей с арностью более двух
(,2,) 1 3 == (1,2,3)
картографирование
Это может быть полезно в других местах, где используются разделы:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
Вышеприведенный пример без этого расширения будет выглядеть так:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Расширение, позволяющее использовать символы Unicode вместо определенных встроенных операторов и имен.
ASCII | Unicode | Используйте (s) |
---|---|---|
:: | ∷ | имеет тип |
-> | → | типы функций, лямбда, ветви case и т. д. |
=> | ⇒ | ограничения класса |
forall | ∀ | явный полиморфизм |
<- | ← | do запись |
* | ★ | тип (или вид) типов (например, Int :: ★ ) |
>- | ⤚ | proc для Arrows |
-< | ⤙ | proc для Arrows |
>>- | ⤜ | proc для Arrows |
-<< | ⤛ | proc для Arrows |
Например:
runST :: (forall s. ST s a) -> a
станет
runST ∷ (∀ s. ST s a) → a
Обратите внимание, что пример *
vs. ★
немного отличается: поскольку *
не зарезервирован, ★
также работает так же, как *
для умножения или любой другой функции с именем (*)
и наоборот. Например:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
BinaryLiterals
Стандартный Haskell позволяет записывать целочисленные литералы в десятичных (без префикса), шестнадцатеричные (с предшествующими 0x
или 0X
) и восьмеричные (с предшествующими 0o
или 0O
). Расширение BinaryLiterals
добавляет возможность двоичного кода (которому предшествуют 0b
или 0B
).
0b1111 == 15 -- evaluates to: True
ExistentialQuantification
Это системное расширение типа, которое позволяет типы, которые экзистенциально квантуются, или, другими словами, иметь переменные типа, которые получают только экземпляр во время выполнения † .
Значение экзистенциального типа похоже на ссылку на абстрактный базовый класс в языках OO: вы не знаете точный тип в составе, но можете ограничить класс типов.
data S = forall a. Show a => S a
или эквивалентно, с синтаксисом GADT:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Экзистенциальные типы открывают дверь для вещей, таких как почти гетерогенные контейнеры: как сказано выше, на самом деле могут быть разные типы в значении S
, но все они могут быть show
n, следовательно, вы также можете сделать
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Теперь мы можем создать коллекцию таких объектов:
ss = [S 5, S "test", S 3.0]
Что также позволяет использовать полиморфное поведение:
mapM_ print ss
Экзистенциалы могут быть очень мощными, но обратите внимание, что они на самом деле не нужны очень часто в Haskell. В приведенном выше примере все, что вы действительно можете сделать с экземпляром Show
- это показать (duh!) Значения, т. Е. Создать строковое представление. Таким образом, весь S
тип содержит точно столько же информации, сколько строка, которую вы получаете при ее показе. Поэтому обычно лучше просто сохранить эту строку сразу, тем более, что Haskell ленив, и поэтому строка вначале будет только неоценимым thunk.
С другой стороны, экзистенции вызывают некоторые уникальные проблемы. Например, способ, которым информация типа «скрыта» в экзистенциальном. Если вы сопоставляете шаблон по значению S
, у вас будет скрытый тип в области видимости (точнее, его экземпляр Show
), но эта информация никогда не сможет выйти из сферы действия, поэтому становится немного «секретным обществом»: компилятор не позволяет чему-либо избежать рамки, кроме значений, тип которых уже известен извне. Это может привести к появлению странных ошибок, таких как Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Контрастируйте это с обычным параметрическим полиморфизмом, который обычно разрешается во время компиляции (разрешая полное стирание типа).
Экзистенциальные типы отличаются от типов Rank-N - эти расширения, грубо говоря, двойственны друг к другу: чтобы фактически использовать значения экзистенциального типа, вам нужна (возможно, ограниченная) полиморфная функция, как show
в примере. Полиморфная функция универсально квантифицирована, т. Е. Работает для любого типа в данном классе, тогда как экзистенциальная квантификация означает, что она работает для определенного типа, который является априорным неизвестным. Если у вас есть полиморфная функция, этого достаточно, однако, чтобы передавать полиморфные функции, такие как аргументы, вам нужно {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Синтаксическое расширение, которое позволяет вам писать \case
вместо \arg -> case arg of
.
Рассмотрим следующее определение функции:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Если вы хотите избежать повторения имени функции, вы можете написать что-то вроде:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Используя расширение LambdaCase, вы можете написать это как выражение функции, не указывая аргумент:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Представьте себе следующую ситуацию:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Здесь мы хотим передать функцию, которая преобразует значение в строку, применить эту функцию как к параметру строки, так и к параметру int и распечатать их оба. На мой взгляд, нет причин, по которым это должно потерпеть неудачу! У нас есть функция, которая работает с обоими типами параметров, которые мы передаем.
К сожалению, это не будет проверять! GHC выводит a
тип , основанный от его первого появления в теле функции. То есть, как только мы ударим:
putStrLn (show' string)
GHC выведет, что show' :: String -> String
, так как string
является String
. Он будет продолжать взрываться, пытаясь show' int
.
RankNTypes
позволяет вместо этого писать сигнатуру типа следующим образом, количественно определяя все функции, которые удовлетворяют типу show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
Это ранг 2 полиморфизм: Мы утверждаем , что show'
функция должна работать для всех a
е в нашей функции, а предыдущая реализация в настоящее время работает.
Расширение RankNTypes
позволяет произвольно RankNTypes
блоки forall ...
в сигнатуры типов. Другими словами, он допускает полиморфизм ранга N.
OverloadedLists
добавлено в GHC 7.8 .
OverloadedLists, аналогичный OverloadedStrings , позволяет выводить литералы списков следующим образом:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Это удобно при работе с такими типами, как Set
, Vector
и Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
Класс IsList
в GHC.Exts
предназначен для использования с этим расширением.
IsList
оснащен одной функцией типа, Item
и тремя функциями: fromList :: [Item l] -> l
, toList :: l -> [Item l]
и fromListN :: Int -> [Item l] -> l
где fromListN
является обязательным. Типичными реализациями являются:
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
Примеры, взятые из OverloadedLists - GHC .
FunctionalDependencies
Если у вас есть многопараметрический тип-класс с аргументами a, b, c и x, это расширение позволяет вам выразить, что тип x может быть однозначно идентифицирован из a, b и c:
class SomeClass a b c x | a b c -> x where ...
При объявлении экземпляра такого класса он будет проверяться на всех других экземплярах, чтобы убедиться, что функциональная зависимость выполняется, то есть не существует другого экземпляра с тем же abc
но отличается x
.
Вы можете указать несколько зависимостей в списке, разделенном запятыми:
class OtherClass a b c d | a b -> c d, a d -> b where ...
Например, в MTL мы видим:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Теперь, если у вас есть значение типа MonadReader a ((->) Foo) => a
, компилятор может сделать вывод, что a ~ Foo
, так как второй аргумент полностью определяет первый и упростит тип соответственно.
Класс SomeClass
можно рассматривать как функцию аргументов abc
что приводит к x
. Такие классы могут использоваться для выполнения вычислений в системе типов.
GADTs
Обычные алгебраические типы данных являются параметрическими в своих переменных типа. Например, если мы определим ADT как
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
с надеждой, что это будет статически исключать не-типизированные условные IntLit :: Int -> Expr a
, это не будет вести себя так, как ожидалось, так как тип IntLit :: Int -> Expr a
: для любого выбора a
он производит значение типа Expr a
. В частности, для a ~ Bool
у нас есть IntLit :: Int -> Expr Bool
, что позволяет нам построить что-то вроде If (IntLit 1) e1 e2
что и пыталось исключить тип конструктора If
.
Обобщенные алгебраические типы данных позволяют нам управлять результирующим типом конструктора данных, чтобы они были не просто параметрическими. Мы можем переписать наш тип Expr
как GADT следующим образом:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Здесь тип конструктора IntLit
- Int -> Expr Int
, и поэтому IntLit 1 :: Expr Bool
не будет проверяться typecheck.
Согласование шаблонов по значению GADT вызывает уточнение типа возвращаемого термина. Например, можно написать оценщик для Expr a
следующим образом:
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
Обратите внимание, что мы можем использовать (+)
в приведенных выше определениях, потому что, когда, например, IntLit x
соответствует шаблону, мы также узнаем, что a ~ Int
(а также для not
и if_then_else_
когда a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
позволяет ссылаться на универсально квантифицированные типы внутри декларации. Чтобы быть более явным:
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)
Важно то, что мы можем использовать a
, b
и c
чтобы проинструктировать компилятор в подвыражениях объявления (кортеж в предложении where
и первый a
в конечном результате). На практике ScopedTypeVariables
помогает записывать сложные функции в виде суммы частей, позволяя программисту добавлять сигнатуры типов к промежуточным значениям, которые не имеют конкретных типов.
PatternSynonyms
Синонимы шаблонов - это абстракции шаблонов, аналогичные тем, как функции являются абстракциями выражений.
В этом примере давайте посмотрим на интерфейс Data.Sequence
, и давайте посмотрим, как его можно улучшить с помощью синонимов шаблонов. Тип Seq
- это тип данных, который внутренне использует сложное представление для достижения хорошей асимптотической сложности для различных операций, в первую очередь как O (1) (un) consing, так и (un) snocing.
Но это представление громоздко и некоторые его инварианты не могут быть выражены в системе типов Хаскелла. Из-за этого тип Seq
подвергается воздействию пользователей как абстрактного типа, наряду с сохраняющими инварианты функциями доступа и конструкторами, среди которых:
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
Но использование этого интерфейса может быть немного громоздким:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Мы можем использовать шаблоны просмотра, чтобы немного почистить его:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Используя PatternSynonyms
языка PatternSynonyms
, мы можем дать еще более приятный интерфейс, позволяя сопоставлять шаблоны, чтобы притворяться, что у нас есть cons- или snoc-list:
{-# 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)
Это позволяет нам писать uncons
в очень естественном стиле:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
См. RecordWildCards