do
a <- ma
=>
ma >>= (a -> exp)
exp
do
exp1
=>
exp1 >> exp2
exp2
do
let x = fx
=>
let x = fx
y = fy
y = fy
exp
in
exp
Переведём с помощью этих правил определение для второго уравнения из функции sequence
sequence (mx:mxs) = do
x
<- mx
mx >>= (x -> do
xs
<- sequence mxs
=>
xs <- sequence mxs
=>
return (x:xs)
return (x:xs))
=>
mx >>= (x -> sequence mxs >>= (xs -> return (x:xs)))
do или Applicative?
С появлением класса Applicative во многих случаях do-нотация теряет свою ценность. Так например
любой do-блок вида:
f mx my = do
x <- mx
y <- my
return (op x y)
Можно записать гораздо короче:
f = liftA2 op
Например напишем функцию, которая объединяет два файла в один:
appendFiles :: FilePath -> FilePath -> FilePath -> IO ()
С помощью do-нотации:
appendFiles file1 file2 resFile = do
a <- readFile file1
b <- readFile file2
writeFile resFile (a ++ b)
А теперь с помощью класса Applicative:
appendFiles file1 file2 resFile = writeFile resFile =<<
liftA2 (++) (readFile file1) (readFile file2)
Пуд сахара | 253
17.2 Расширения
Расширение появляется в ответ на проблему, с которой трудно или невозможно справится в рамках стан-
дарта Haskell. Мы рассмотрим несколько наиболее часто используемых расширений. Расширения подключа-
ются с помощью специального комментария. Он помещается в начале модуля. Расширение действует только
в текущем модуле.
{-# LANGUAGE
ExtentionName1, ExtentionName2, ExtentionName3 #-}
Обратите внимание на символ решётка, обрамляющие комментарии. Слово LANGUAGE говорит компи-
лятору о том, что мы хотим воспользоваться расширениями с именами ExtentionName1, ExtentionName2,
ExtentionName3. Такой комментарий называется прагмой (pragma). Часто компилятор ghc в случае ошибки
предлагает нам подключить расширение, в котором ошибка уже не будет ошибкой, а возможностью языка.
Он говорит возможно вы имели в виду расширение XXX. Например попробуйте загрузить в интерпретатор
модуль:
module Test where
class Multi a b where
В этом случае мы увидим ошибку:
Prelude> :l Test
[1 of 1] Compiling Test
( Test. hs, interpreted )
Test. hs:3:0:
Too many parameters for class ‘Multi’
(Use -XMultiParamTypeClasses to allow multi—parameter classes)
In the class declaration for ‘Multi’
Failed, modules loaded: none.
Компилятор сообщает нам о том, что у нас слишком много параметров в классе Multi. В рамках стандар-
та Haskell можно создавать лишь классы с одним параметром. Но за сообщением мы видим подсказку, если
мы воспользуемся расширением -XMultiParamTypeClasses, то всё будет хорошо. В этом сообщении имя рас-
ширения закодировано в виде флага. Мы можем запустить ghc или ghci с этим флагом и тогда расширение
будет активировано, и модуль загрузится. Попробуем:
Prelude> :q
Leaving GHCi.
$ ghci -XMultiParamTypeClasses
Prelude> :l Test
[1 of 1] Compiling Test
( Test. hs, interpreted )
Ok, modules loaded: Test.
*Test>
Модуль загрузился! У нас есть и другая возможность подключить модуль с помощью прагмы LANGUAGE.
Имя расширения записано во флаге после символов -X. Добавим в модуль Test расширение с именем
MultiParamTypeClasses:
{-# LANGUAGE MultiParamTypeClasses #-}
module Test where
class Multi a b where
Теперь загрузим ghci в обычном режиме:
*Test> :q
Leaving GHCi.
$ ghci
Prelude> :l Test
[1 of 1] Compiling Test
( Test. hs, interpreted )
Ok, modules loaded: Test.
254 | Глава 17: Дополнительные возможности
Обобщённые алгебраические типы данных
Предположим, что мы хотим написать компилятор небольшого языка. Наш язык содержит числа и логиче-
ские значения. Мы можем складывать числа и умножать. Для логических значений определена конструкция
if-then-else. Определим тип синтаксического дерева для этого языка:
data Exp = ValTrue
| ValFalse
| If Exp Exp Exp
| Val Int
| Add Exp Exp
| Mul Exp Exp
deriving (Show)
В этом определении кроется одна проблема. Наш тип позволяет нам строить бессмысленные выражения
вроде Add ValTrue (Val 2) или If (Val 1) ValTrue (Val 22). Наш тип Val включает в себя все хорошие вы-
ражения и много плохих. Эта проблема проявится особенно ярко, если мы попытаемся определить функцию
eval, которая вычисляет значение для нашего языка. Получается, что тип этой функции:
eval :: Exp -> Either Int Bool
Для решения этой проблемы были придуманы обобщённые алгебраические типы данных (generalised
algebraic data types, GADTs). Они подключаются расширением GADTs. Помните когда-то мы говорили, что
типы можно представить в виде классов. Например определение для списка
data List a = Nil | Cons a (List a)
можно мысленно переписать так:
data List a where
Nil
:: List a
Cons :: a -> List a -> List a
Так вот в GADT определения записываются именно в таком виде. Обобщение заключается в том, что
теперь на месте произвольного параметра a мы можем писать конкретные типы. Определим тип GExp
{-# LANGUAGE GADTs #-}
data Exp a where
ValTrue
:: Exp Bool
ValFalse
:: Exp Bool
If
:: Exp Bool -> Exp a -> Exp a -> Exp a
Val
:: Int -> Exp Int
Add
:: Exp Int -> Exp Int -> Exp Int
Mul
:: Exp Int -> Exp Int -> Exp Int
Теперь у нашего типа Exp появился параметр, через который мы кодируем дополнительные ограничения
на типы операций. Теперь мы не сможем составить выражение Add ValTrue ValFalse, потому что оно не
пройдёт проверку типов.
Определим функцию eval:
eval :: Exp a -> a
eval x = case x of
ValTrue
-> True
ValFalse
-> False
If p t e
-> if eval p then eval t else eval e
Val n
-> n
Add a b
-> eval a + eval b
Mul a b
-> eval a * eval b
Если eval получит логическое значение, то будет возвращено значение типа Bool, а на значение типа Exp
Int будет возвращено целое число. Давайте убедимся в этом:
Расширения | 255
*Prelude> :l Exp
[1 of 1] Compiling Exp
( Exp. hs, interpreted )
Ok, modules loaded: Exp.
*Exp> let notE x = If x ValFalse ValTrue
*Exp> let squareE x = Mul x x
*Exp>
*Exp> eval $ squareE $ If (notE ValTrue) (Val 1) (Val 2)
4
*Exp> eval $ notE ValTrue
False
*Exp> eval $ notE $ Add (Val 1) (Val 2)
< interactive>:1:14:
Couldn’t match expected type ‘Bool’ against inferred type ‘Int’
Expected type: Exp Bool
Actual type: Exp Int
In the return type of a call of ‘Add’
In the second argument of ‘($)’, namely ‘Add (Val 1) (Val 2)’
Сначала мы определили две вспомогательные функции. Затем вычислили несколько значений. Haskell
очень часто применяется для построения компиляторов. Мы рассмотрели очень простой язык, но в более
сложном случае суть останется прежней. Дополнительный параметр позволяет нам закодировать в парамет-
ре тип функций нашего языка. Спрашивается: зачем нам дублировать вычисления в функции eval? Зачем нам
сначала кодировать выражение конструкторами, чтобы только потом получить то, что мы могли вычислить
и напрямую.
При таком подходе у нас есть полный контроль за деревом выражения, мы можем проводить дополни-
тельную оптимизацию выражений, если нам известны некоторые закономерности. Ещё функция eval может
вычислять совсем другие значения. Например она может по виду выражения составлять код на другом языке.
Возможно этот язык гораздо мощнее Haskell по вычислительным способностям, но беднее в плане вырази-
тельности, гибкости синтаксиса. Тогда мы будем в функции eval проецировать разные конструкции Haskell





