Функторы и монады: примеры

В этой главе мы закрепим на примерах то, что мы узнали о монадах и функторах. Напомню, что с помощью монад и функторов мы можем комбинировать специальные функции вида (a -> m b) с другими специальными функциями.

У нас есть функции тождества (pure, return) и применения (fmap, =<<):

class Functor f where
    fmap :: (a -> b) -> f a -> f b

class Functor f => Applicative f where
    pure    :: a -> f a
    (<*>)   :: f (a -> b) -> f a -> f b

class Monad m where
    return  :: a -> m a
    (>>=)   :: m a -> (a -> m b) -> m b

(=<<) :: (a -> m b) -> m a -> m b
(=<<) = flip (>>=)

Вспомним основные производные функции для этих классов:

Или в терминах класса Kleisli:

-- Композиция
(>=>) :: Monad m => (a -> m b) -> (b -> m c) -> (a -> m c)
(<=<) :: Monad m => (b -> m c) -> (a -> m b) -> (a -> m c)

-- Константные функции
(*>) :: Applicative f => f a -> f b -> f b
(<*) :: Applicative f => f a -> f b -> f a

-- Применение обычных функций к специальным значениям
(<$>)  :: Functor f => (a -> b) -> f a -> f b

liftA  :: Applicative f => (a -> b)           -> f a -> f b
liftA2 :: Applicative f => (a -> b -> c)      -> f a -> f b -> f c
liftA3 :: Applicative f => (a -> b -> c -> d) -> f a -> f b -> f c -> f d

-- Преобразование элементов списка специальной функцией
mapM   :: Monad m => (a -> m b) -> [a] -> m [b]

Нам понадобится модуль с определениями типов и экземпляров монад для всех типов, которые мы рассмотрели в предыдущей главе. Экземпляры для [] и Maybe уже определены в Prelude, а типы State, Reader и Writer можно найти в библиотеках mtl и transformers. Пока мы не знаем как устанавливать библиотеки, определим эти типы и экземпляры для Monad самостоятельно. Возможно, вы уже определили их, выполняя одно из упражнений предыдущей главы, если это так, то сейчас вы можете сверить ответы. Определим модуль Types:

module Types(
    State(..), Reader(..), Writer(..),
    runState, runWriter, runReader,
    module Control.Applicative,
    module Control.Monad,
    module Data.Monoid)    
where

import Data.Monoid
import Control.Applicative
import Control.Monad

-------------------------------------------------
-- Функции с состоянием
--
--      a -> State s b

data State s a = State (s -> (a, s))

runState :: State s a -> s -> (a, s)
runState (State f) = f

instance Monad (State s) where
    return a  = State $ \s -> (a, s)
    ma >>= mf = State $ \s0 -> 
                    let (b, s1) = runState ma s0
                    in   runState (mf b) s1

---------------------------------------------------
-- Функции с окружением
--
--      a -> Reader env b

data Reader env a = Reader (env -> a)

runReader :: Reader env a -> env -> a
runReader (Reader f) = f

instance Monad (Reader env) where
    return a    = Reader $ const a
    ma >>= mf   = Reader $ \env -> 
                    let b = runReader ma env
                    in  runReader (mf b) env 
                    
---------------------------------------------------
-- Функции-накопители
--
--      Monoid msg => a -> Writer msg b

data Writer msg a = Writer (a, msg)
    deriving (Show)

runWriter :: Writer msg a -> (a, msg)
runWriter (Writer f) = f

instance Monoid msg => Monad (Writer msg) where
    return a    = Writer (a, mempty)
    ma >>= mf   = Writer (c, msgA `mappend` msgF)
        where (b, msgA) = runWriter ma
              (c, msgF) = runWriter $ mf b

Я пропустил определения для экземпляров классов Functor и Applicative, их можно получить из экземпляра для класса Monad с помощью стандартных функций liftM, return и ap из модуля Control.Monad.

Нам встретилась новая запись в экспорте модуля. Для удобства мы экспортируем модули Control.Applicative, Control.Monad и Data.Monoid целиком. Для этого мы написали ключевое слово module перед экспортируемым модулем. Теперь если мы в каком-нибудь другом модуле импортируем модуль Types нам станут доступными все функции из этих модулей.

Случайные числа

С помощью монады State можно имитировать случайные числа. Мы будем генерировать случайные числа из интервала от 0 до 1 с помощью алгоритма:

nextRandom :: Double -> Double
nextRandom = snd . properFraction . (105.947 * )

Функция properFraction возвращает пару, которая состоит из целой части и остатка числа. Взяв второй элемент пары с помощью snd, мы выделяем остаток. Функция nextRandom представляет собой генератор случайных чисел, который принимает значение с предыдущего шага и строит по нему следующее значение.

Построим тип для случайных чисел:

type Random a = State Double a

next :: Random Double
next = State $ \s -> (s, nextRandom s)

Теперь определим функцию, которая прибавляет к данному числу случайное число из интервала от 0 до 1:

addRandom :: Double -> Random Double
addRandom x = fmap (+x) next 

Посмотрим как эта функция работает в интерпретаторе:

*Random> runState (addRandom 5) 0.5
(5.5,0.9735000000000014)
*Random> runState (addRandom 5) 0.7
(5.7,0.16289999999999338)
*Random> runState (mapM addRandom [1 .. 5]) 0.5
([1.5,2.9735000000000014,3.139404500000154,4.769488561516319,
 5.5250046269694195],0.6226652135290891)

В последней строчке мы с помощью функции mapM прибавили ко всем элементам списка разные случайные числа, обновление счётчика происходило за кадром, с помощью функции mapM и экземпляра Monad для State.

Также мы можем определить функцию, которая складывает два случайных числа, одно из интервала [-1+a, 1+a], а другое из интервала [-2+b,2+b]:

addRandom2 :: Double -> Double -> Random Double
addRandom2 a b = liftA2 add next next
    where add  a b = \x y -> diap a 1 x + diap b 1 y
          diap c r = \x   -> x * 2 * r - r + c

Функция diap перемещает интервал от 0 до 1 в интервал от c-r до c+r. Обратите внимание на то как мы сначала составили обычную функцию add, которая перемещает значения из интервала от 0 до 1 в нужный диапазон и складывает. И только в самый последний момент мы применили к этой функции случайные значения. Посмотрим как работает эта функция:

*Random> runState (addRandom2 0 10) 0.5
(10.947000000000003,0.13940450000015403)
*Random> runState (addRandom2 0 10) 0.7
(9.725799999999987,0.2587662999992979)

Прибавим два списка и получим сумму:

*Random> let res = fmap sum $ zipWithM addRandom2 [1..3] [11 .. 13]
*Random> runState res 0.5
(43.060125804029965,0.969511377766409)
*Random> runState res 0.7
(39.86034841613788,0.26599261421101517)

Функция zipWithM является аналогом функции zipWith. Она устроена также как и функция mapM, сначала применяется обычная функция zipWith, а затем функция sequence.

С помощью типа Random мы можем определить функцию подбрасывания монетки:

data Coin = Heads | Tails
    deriving (Show)

dropCoin :: Random Coin
dropCoin = fmap drop' next
    where drop' x 
            | x < 0.5   = Heads
            | otherwise = Tails                    

У монетки две стороны орёл (Heads) и решка (Tails). Поскольку шансы на выпадание той или иной стороны равны, мы для определения стороны разделяем интервал от 0 до 1 в равных пропорциях.

Подбросим монетку пять раз:

*Random> let res = sequence $ replicate 5 dropCoin

Функция replicate n a составляет список из n повторяющихся элементов a. Посмотрим что у нас получилось:

*Random> runState res 0.4
([Heads,Heads,Heads,Heads,Tails],0.5184926967068364)
*Random> runState res 0.5
([Tails,Tails,Heads,Tails,Tails],0.6226652135290891)

Конечные автоматы

С помощью монады State можно описывать конечные автоматы (finite-state machine). Конечный автомат находится в каком-то начальном состоянии. Он принимает на вход ленту событий. Одно событие происходит за другим. На каждое событие автомат реагирует переходом из одного состояния в другое.

type FSM s = State s s

fsm :: (ev -> s -> s) -> (ev -> FSM s)
fsm transition = \e -> State $ \s -> (s, transition e s)

Функция fsm принимает функцию переходов состояний transition и возвращает функцию, которая принимает состояние и возвращает конечный автомат. В качестве значения конечный автомат FSM будет возвращать текущее состояние.

С помощью конечных автоматов можно описывать различные устройства. Лентой событий будет ввод пользователя (нажатие на кнопки, включение/выключение питания).

Приведём простой пример. Рассмотрим колонки, у них есть розетка, кнопка вкл/выкл и регулятор громкости. Возможные состояния:

type Speaker = (SpeakerState, Level)

data SpeakerState = Sleep | Work
    deriving (Show)

data Level  = Level Int
    deriving (Show)

Тип колонок складывается из двух значений: состояния и уровня громкости. Колонки могут быть выключенными (Sleep) или работать на определённой громкости (Work). Считаем, что максимальный уровень громкости составляет 10 единиц, а минимальный ноль единиц. Границы диапазона громкости описываются такими функциями:

quieter :: Level -> Level
quieter (Level n) = Level $ max 0 (n-1)

louder :: Level -> Level
louder (Level n) = Level $ min 10 (n+1)

Мы будем обновлять значения уровня громкости не напрямую, а с помощью вспомогательных функций louder и quieter. Так мы не сможем выйти за пределы заданного диапазона.

Возможные события:

data User = Button | Quieter | Louder
    deriving (Show)

Пользователь может либо нажать на кнопку вкл/выкл или повернуть реле громкости влево, чтобы приглушить звук (Quieter) или вправо, чтобы сделать погромче (Louder). Будем считать, что колонки всегда включены в розетку.

Составим функцию переходов:

speaker :: User -> FSM Speaker
speaker = fsm $ trans
    where trans Button    (Sleep, n) = (Work, n)
          trans Button    (Work,  n) = (Sleep, n)
          trans Louder    (s,     n) = (s, louder n)
          trans Quieter   (s,     n) = (s, quieter n)

Мы считаем, что при выключении колонок реле остаётся некотором положении, так что при следующем включении они будут работать на той же громкости. Реле можно крутить и в состоянии Sleep. Посмотрим на типичную сессию работы колонок:

*FSM> let res = mapM speaker [Button, Louder, Quieter, Quieter, Button] 

Сначала мы включаем колонки, затем прибавляем громкость, затем дважды делаем тише и в конце выключаем. Посмотрим что получилось:

*FSM> runState res (Sleep, Level 2)
([(Sleep,Level 2),(Work,Level 2),(Work,Level 3),(Work,Level 2),
 (Work,Level 1)],(Sleep,Level 1))
*FSM> runState res (Sleep, Level 0)
([(Sleep,Level 0),(Work,Level 0),(Work,Level 1),(Work,Level 0),
 (Work,Level 0)],(Sleep,Level 0))

Смотрите, изменив начальное значение, мы изменили весь список значений. Обратите внимание на то, что во втором прогоне мы не ушли в минус по громкости, не смотря на то, что пытались крутить реле за установленный предел.

Определим колонки другого типа. Наши новые колонки будут безопаснее предыдущих. Представьте ситуацию, что мы выключили колонки на высоком уровне громкости. Мы слушали домашнюю запись с низким уровнем звука. Мы выключили и забыли. Потом мы решили послушать другую мелодию, которая записана с нормальным уровнем звука. При включении колонок нас оглушил шквал звука. Чтобы этого избежать мы решили воспользоваться другими колонками.

Колонки при выключении будут выставлять уровень громкости на ноль и реле можно будет крутить только если колонки включены.

safeSpeaker :: User -> FSM Speaker
safeSpeaker = fsm $ trans
    where trans Button  (Sleep, _) = (Work,  Level 0)
          trans Button  (Work,  _) = (Sleep, Level 0)
          trans Quieter (Work,  n) = (Work,  quieter n)
          trans Louder  (Work,  n) = (Work,  louder n)
          trans _       (Sleep, n) = (Sleep, n)

При нажатии на кнопку вкл/выкл уровень громкости выводится в положение 0. Колонки реагируют на запросы изменения уровня громкости только в состоянии Work. Посмотрим как работают наши новые колонки:

*FSM> let res = mapM safeSpeaker [Button, Louder, Quieter, Button, Louder]

Мы включаем колонки, делаем по-громче, затем по-тише, затем выключаем и пытаемся изменить громкость после выключения. Посмотрим как они сработают, представим, что мы выключили колонки на уровне громкости 10:

*FSM> runState res (Sleep, Level 10)
([(Sleep,Level 10),(Work,Level 0),(Work,Level 1),(Work,Level 0),
 (Sleep,Level 0)],(Sleep,Level 0))

Первое значение в списке является стартовым состоянием, которое мы задали. После этого колонки включаются и мы видим, что уровень громкости переключился на ноль. Затем мы увеличиваем громкость, сбавляем её и выключаем. Попытка изменить громкость выключенных колонок не проходит. Это видно по последнему элементу списка и итоговому состоянию колонок, которое находится во втором элементе пары.

Предположим, что колонки работают с самого начала, тогда первым действием мы выключаем их. Посмотрим, что случится дальше:

*FSM> runState res (Work, Level 10)
([(Work,Level 10),(Sleep,Level 0),(Sleep,Level 0),(Sleep,Level 0),
 (Work,Level 0)],(Work,Level 1))

Дальше мы пытаемся изменить громкость но у нас ничего не выходит.

Отложенное вычисление выражений

В этом примере мы будем выполнять арифметические операции на целых числах. Мы будем их складывать, вычитать и умножать. Но вместо того, чтобы сразу вычислять выражения мы будем составлять их описание. Мы будем кодировать операции конструкторами.

data Exp    = Var String
            | Lit Int
            | Neg Exp
            | Add Exp Exp
            | Mul Exp Exp
            deriving (Show, Eq)

У нас есть тип Exp, который может быть либо переменной Var с данным строчным именем, либо целочисленной константой Lit, либо одной из трёх операций: вычитанием (Neg), сложением (Add) или умножением (Mul).

Такие типы называют абстрактными синтаксическими деревьями (abstract syntax tree, AST). Они содержат описание выражений. Теперь вместо того чтобы сразу проводить вычисления мы будем собирать выражения в значении типа Exp. Сделаем экземпляр для Num:

instance Num Exp where
    negate  = Neg
    (+)     = Add
    (*)     = Mul

    fromInteger = Lit . fromInteger

    abs     = undefined
    signum  = undefined

Также определим вспомогательные функции для обозначения переменных:

var :: String -> Exp
var = Var

n :: Int -> Exp
n = var . show

Функция var составляет переменную с данным именем, а функция n составляет переменную, у которой имя является целым числом. Сохраним эти определения в модуле Exp. Теперь у нас всё готово для составления выражений:

*Exp> n 1
Var "1"
*Exp> n 1 + 2
Add (Var "1") (Lit 2)
*Exp> 3 * (n 1 + 2)
Mul (Lit 3) (Add (Var "1") (Lit 2))
*Exp> - n 2 * 3 * (n 1 + 2)
Neg (Mul (Mul (Var "2") (Lit 3)) (Add (Var "1") (Lit 2)))

Теперь давайте создадим функцию для вычисления таких выражений. Она будет принимать выражение и возвращать целое число.

eval :: Exp -> Int
eval (Lit n)    = n
eval (Neg n)    = negate $ eval n
eval (Add a b)  = eval a + eval b
eval (Mul a b)  = eval a * eval b
eval (Var name) = ???

Как быть с конструктором Var? Нам нужно откуда-то узнать какое значение связано с переменной. Функция eval должна также принимать набор значений для всех переменных, которые используются в выражении. Этот набор значений мы будем называть окружением.

Обратите внимание на то, что в каждом составном конструкторе мы рекурсивно вызываем функцию eval, мы словно обходим всё дерево выражения. Спускаемся вниз, до самых листьев в которых расположены либо значения (Lit), либо переменные (Var). Нам было бы удобно иметь возможность пользоваться окружением из любого узла дерева. В этом нам поможет тип Reader.

Представим что у нас есть значение типа Env и функция, которая позволяет читать значения переменных по имени:

value :: Env -> String -> Int

Теперь определим функцию eval:

eval :: Exp -> Reader Env Int
eval (Lit n)    = pure n
eval (Neg n)    = liftA  negate $ eval n
eval (Add a b)  = liftA2 (+) (eval a) (eval b)
eval (Mul a b)  = liftA2 (*) (eval a) (eval b)
eval (Var name) = Reader $ \env -> value env name 

Определение сильно изменилось, оно стало не таким наглядным. Теперь значение eval стало специальным, поэтому при рекурсивном вызове функции eval нам приходится поднимать в мир специальных функций обычные функции вычитания, сложения и умножения. Мы можем записать это выражение
немного по другому:

eval :: Exp -> Reader Env Int
eval (Lit n)    = pure n
eval (Neg n)    = negateA $ eval n
eval (Add a b)  = eval a `addA` eval b
eval (Mul a b)  = eval a `mulA` eval b
eval (Var name) = Reader $ \env -> value env name    
   
addA      = liftA2 (+)
mulA      = liftA2 (*)  
negateA   = liftA negate  

Тип Map

Для того чтобы закончить определение функции eval нам нужно определить тип Env и функцию value. Для этого мы воспользуемся типом Map, он предназначен для хранения значений по ключу.

Этот тип живёт в стандартном модуле Data.Map. Посмотрим на его описание:

data Map k a = ..

Первый параметр типа k это ключ, а второй это значение. Мы можем создать значение типа Map из списка пар ключ значение с помощью функции fromList.

Посмотрим на основные функции:

-- Создаём значения типа Map                    -- создаём  
empty :: Map k a                                -- пустой Map
fromList :: Ord k => [(k, a)] -> Map k a        -- по списку (ключ, значение)

-- Узнаём значение по ключу
(!)     :: Ord k => Map k a -> k -> a           
lookup  :: Ord k => k -> Map k a -> Maybe a

-- Добавляем элементы
insert :: Ord k => k -> a -> Map k a -> Map k a

-- Удаляем элементы
delete :: Ord k => k -> Map k a -> Map k a

Обратите внимание на ограничение Ord k в этих функциях, ключ должен быть экземпляром класса Ord. Посмотрим как эти функции работают:

*Exp> :m +Data.Map
*Exp Data.Map> :m -Exp
Data.Map> let v = fromList [(1, "Hello"), (2, "Bye")]
Data.Map> v ! 1
"Hello"
Data.Map> v ! 3
"*** Exception: Map.find: element not in the map
Data.Map> lookup 3 v
Nothing
Data.Map> let v1 = insert 3 "Yo" v
Data.Map> v1 ! 3
"Yo"

Функция lookup является стабильным аналогом функции !. В том смысле, что она определена с помощью Maybe. Она не приведёт к падению программы, если для данного ключа не найдётся значение.

Теперь мы можем определить функцию value:

import qualified Data.Map as M(Map, lookup, fromList)

...

type Env = M.Map String Int

value :: Env -> String -> Int
value env name = maybe errorMsg $ M.lookup env name 
    where errorMsg = error $ "value is undefined for " ++ name

Обычно функции из модуля Data.Map включаются с директивой qualified, поскольку имена многих функций из этого модуля совпадают с именами из модуля Prelude. Теперь все определения из модуля Data.Map пишутся с приставкой M..

Создадим вспомогательную функцию, которая упростит вычисление выражений:

runExp :: Exp -> [(String, Int)] -> Int
runExp a env = runReader (eval a) $ M.fromList env

Сохраним определение новых функций в модуле Exp. И посмотрим что у нас получилось:

*Exp> let env a b = [("1", a), ("2", b)]
*Exp> let exp = 2 * (n 1 + n 2) - n 1
*Exp> runExp exp (env 1 2)
5
*Exp> runExp exp (env 10 5)
20

Так мы можем пользоваться функциями с окружением для того, чтобы читать значения из общего источника. Впрочем мы можем просто передавать окружение дополнительным аргументом и не пользоваться монадами:

eval :: Env -> Exp -> Int
eval env x = case x of
    Lit n       -> n
    Neg n       -> negate $ eval' n
    Add a b     -> eval' a + eval' b
    Mul a b     -> eval' a + eval' b
    Var name    -> value env name
    where eval' = eval env

Накопление результата

Рассмотрим по-подробнее тип Writer. Он выполняет задачу обратную к типу Reader. Когда мы пользовались типом Reader, мы могли в любом месте функции извлекать данные из окружения. Теперь же мы будем не извлекать данные из окружения, а записывать их.

Рассмотрим такую задачу нам нужно обойти дерево типа Exp и подсчитать все бинарные операции. Мы прибавляем к накопителю результата единицу за каждый конструктор Add или Mul. Тип сообщений будет числом. Нам нужно сделать экземпляр класса Monoid для чисел.

Напомню, что тип накопителя должен быть экземпляром класса Monoid:

class Monoid a where
    mempty  :: a
    mappend :: a -> a -> a

    mconcat :: [a] -> a
    mconcat = foldr mappend mempty

Но для чисел возможно несколько вариантов, которые удовлетворяют свойствам. Для сложения:

instance Num a => Monoid a where
    mempty  = 0
    mappend = (+)

И умножения:

instance Num a => Monoid a where
    mempty  = 1
    mappend = (*)

Для нашей задачи подойдёт первый вариант, но не исключена возможность того, что для другой задачи нам понадобится второй. Но тогда мы уже не сможем определить такой экземпляр. Для решения этой проблемы в модуле Data.Monoid определено два типа обёртки:

newtype Sum  a = Sum  { getSum  :: a }
newtype Prod a = Prod { getProd :: a }

В этом определении есть два новых элемента. Первый это ключевое слово newtype, а второй это фигурные скобки. Что всё это значит?

Тип-обёртка newtype

Ключевое слово newtype вводит новый тип-обёртку. Тип-обёртка может иметь только один конструктор, у которого лишь один аргумент. Запись:

newtype Sum a = Sum a

Это тоже самое, что и

data Sum a = Sum a

Единственное отличие заключается в том, что в случае newtype вычислитель не видит разницы между Sum a и a. Её видит лишь компилятор. Это означает, что на разворачивание и заворачивание такого значения в тип обёртку не тратится никаких усилий. Такие типы подходят для решения двух задач:

Записи

Вторая новинка заключалась в фигурных скобках. С помощью фигурных скобок в Haskell обозначаются записи (records). Запись это произведение типа, но с выделенными именами для полей.

Например мы можем сделать тип для описания паспорта:

data Passport   = Person {
    surname         :: String,      -- Фамилия
    givenName       :: String,      -- Имя
    nationality     :: String,      -- Национальность
    dateOfBirth     :: Date,        -- Дата рождения
    sex             :: Bool,        -- Пол
    placeOfBirth    :: String,      -- Место рождения
    authority       :: String,      -- Место выдачи документа
    dateOfIssue     :: Date,        -- Дата выдачи
    dateOfExpiry    :: Date         -- Дата окончания срока
    } deriving (Eq, Show)           --      действия

data Date   = Date {
                day     :: Int,
                month   :: Int,
                year    :: Int
              } deriving (Show, Eq)

В фигурных скобках через запятую мы указываем поля. Поле состоит из имени и типа. Теперь нам доступны две операции:

Вернёмся к типам Sum и Prod:

newtype Sum  a = Sum  { getSum  :: a }
newtype Prod a = Prod { getProd :: a }

Этой записью мы определили два типа-обёртки. У нас есть две функции, которые заворачивают обычное значение, это Sum и Prod. С помощью записей мы тут же в определении типа определили функции которые разворачивают значения, это getSum и getProd.

Вспомним определение для типа State:

data State s a = State (s -> (a, s))

runState :: State s a -> (s -> (a, s))
runState (State f) = f

Было бы гораздо лучше определить его так:

newtype State s a = State{ runState :: s -> (a, s) }

Накопление чисел

Но вернёмся к нашей задаче. Мы будем накапливать сумму в значении типа Sum. Поскольку нас интересует лишь значение накопителя, наша функция будет возвращать значение единичного типа ().

countBiFuns :: Exp -> Int
countBiFuns = getSum . execWriter . countBiFuns'

countBiFuns' :: Exp -> Writer (Sum Int) ()
countBiFuns' x = case x of
    Add a b -> tell (Sum 1) *> bi a b
    Mul a b -> tell (Sum 1) *> bi a b
    Neg a   -> un a
    _       -> pure ()
    where bi a b = countBiFuns' a *> countBiFuns' b  
          un     = countBiFuns'

tell :: Monoid a => a -> Writer a () 
tell a = Writer ((), a)

execWriter :: Writer msg a -> msg
execWriter (Writer (a, msg)) = msg

Первая функция countBiFuns извлекает значение из типов Writer и Sum. А вторая функция countBiFuns' вычисляет значение.

Мы определили две вспомогательные функции tell, которая записывает сообщение в накопитель и execWriter, которая возвращает лишь сообщение. Это стандартные для Writer функции.

Посмотрим как работает эта функция:

*Exp> countBiFuns (n 2)
0
*Exp> countBiFuns (n 2 + n 1 + 2 + 3)
3

Накопление логических значений

В модуле Data.Monoid определены два типа для накопления логических значений. Это типы All и Any. С помощью типа All мы можем проверить выполняется ли некоторое свойство для всех значений. А с помощью типа Any мы можем узнать, что существует хотя бы один элемент, для которых это свойство выполнено.

Посмотрим на определение экземпляров класса Monoid для этих типов:

newtype All = All { getAll :: Bool }

instance Monoid All where
        mempty = All True
        All x `mappend` All y = All (x && y)

В типе All мы накапливаем значения с помощью логического “и”. Нейтральным элементом является конструктор True. Итоговое значение накопителя будет равно True только в том случае, если все накапливаемые сообщения были равны True.

В типе Any всё наоборот:

instance Monoid Any where
        mempty = Any False
        Any x `mappend` Any y = Any (x || y)

Посмотрим как работают эти типы. Составим функцию, которая проверяет отсутствие оператора минус в выражении:

noNeg :: Exp -> Bool
noNeg = not . getAny . execWriter . anyNeg

anyNeg :: Exp -> Writer Any ()
anyNeg x = case x of
    Neg _   -> tell (Any True)
    Add a b -> bi a b
    Mul a b -> bi a b
    _       -> pure ()
    where bi a b = anyNeg a *> anyNeg b            

Функция anyNeg проверяет есть ли в выражении хотя бы один конструктор Neg. В функции noNeg мы извлекаем результат и берём его отрицание, чтобы убедиться в том что в выражении не встретилось ни одного конструктора Neg.

*Exp> noNeg (n 2 + n 1 + 2 + 3)
True
*Exp> noNeg (n 2 - n 1 + 2 + 3)
False

Накопление списков

Экземпляр класса Monoid определён и для списков. Предположим у нас есть дерево, в каждом узле которого находятся числа, давайте соберём все числа больше 5, но меньше 10. Деревья мы возьмём из модуля Data.Tree:

data Tree a   = Node 
        { rootLabel :: a           -- значение метки
        , subForest :: Forest a    -- ноль или несколько дочерних деревьев
        }

type Forest a = [Tree a]

Интересный тип. Тип Tree определён через Forest, а Forest определён через Tree. По этому типу мы видим, что каждый узел содержит некоторое значение типа a, и список дочерних деревьев.

Составим дерево:

*Exp> :m Data.Tree
Prelude Data.Tree> let t a = Node a []
Prelude Data.Tree> let list a = Node a []
Prelude Data.Tree> let bi v a b = Node v [a, b]
Prelude Data.Tree> let un v a   = Node v [a]
Prelude Data.Tree> 
Prelude Data.Tree> let tree1 = bi 10 (un 2 $ un 6 $ list 7) (list 5)
Prelude Data.Tree> let tree2 = bi 12 tree1 (bi 8 tree1 tree1)

Теперь составим функцию, которая будет обходить дерево, и собирать числа из заданного диапазона:

type Diap a = (a, a)

inDiap :: Ord a => Diap a -> Tree a -> [a]
inDiap d = execWriter . inDiap' d

inDiap' :: Ord a => Diap a -> Tree a -> Writer [a] ()
inDiap' d (Node v xs) = pick d v *> mapM_ (inDiap' d) xs 
    where pick (a, b) v
            | (a <= v) && (v <= b)  = tell [v]
            | otherwise             = pure ()

Как и раньше у нас две функции, одна выполняет вычисления, другая извлекает результат из Writer. В функции pick мы проверяем число на принадлежность интервалу, если это так мы добавляем число к результату, а если нет пропускаем его, добавляя нейтральный элемент (в функции pure). Обратите внимание на то как мы обрабатываем список дочерних поддеревьев. Функция mapM_ является аналогом функции mapM, Она используется, если результат функции не важен, а важны те действия, которые происходят при преобразовании списка. В нашем случае это накопление результата. Посмотрим на определение этой функции:

mapM_ :: Monad m => (a -> m b) ->  [a] -> m ()
mapM_ f = sequence_ . map f

sequence_ :: Monad m => [m a] -> m ()
sequence_ = foldr (>>) (return ())

Основное отличие состоит в функции sequence_. Раньше мы собирали значения в список, а теперь отбрасываем их с помощью константной функции >>. В конце мы возвращаем значение единичного типа ().

Теперь сохраним в модуле Tree определение функции и вспомогательные функции создания деревьев un, bi, и list и посмотрим как наша функция работает:

*Tree> inDiap (4, 10) tree2
[10,6,7,5,8,10,6,7,5,10,6,7,5]
*Tree> inDiap (5, 8) tree2
[6,7,5,8,6,7,5,6,7,5]
*Tree> inDiap (0, 3) tree2
[2,2,2]

Монада изменяемых значений ST

Возможно читатели, для которых “родным” является один из императивных языков, немного заскучали по изменяемым значениям. Мы говорили, что в Haskell ничего не изменяется, мы даём всё более и более сложные имена статическим значениям, а потом вычислитель редуцирует имена к настоящим значениям. Но есть алгоритмы, которые очень элегантно описываются в терминах изменяемых значений. Примером такого алгоритма может быть быстрая сортировка. Задача состоит в перестановке элементов массива так, чтобы на выходе любой последующий элемент массива был больше предыдущего (для списков эту задачу решают функции sort и sortBy).

Само по себе явление обновления значения является побочным эффектом. Оно ломает представление о статичности мира, у нас появляются фазы: до обновления и после обновления. Но представьте, что обновление происходит локально, мы постоянно меняем только одно значение, при этом за время обновления ни одна другая переменная не может пользоваться промежуточными значениями и обновления происходят с помощью чистых функций. Представьте функцию, которая принимает значение, выделяет внутри себя память, и при построении результата начинает обновлять значение внутри этой памяти (с помощью чистых функций) и считать что-то ещё полезное на основе этих обновлений, как только вычисления закончатся, память стирается, и возвращается значение. Будет ли такая функция чистой? Интуиция подсказывает, что да. Это было доказано, но для реализации этого требуется небольшой трюк на уровне типов. Получается, что не смотря на то, что функция содержит побочные эффекты, она является чистой, поскольку все побочные эффекты локальны, они происходят только внутри вызова функции и только в самой функции.

Для симуляции обновления значения в Haskell нам нужно решить две проблемы. Как упорядочить обновление значения? И как локализовать его? В императивных языках порядок вычисления выражений строго связан с порядком следования выражений, на примитивном уровне, грубо упрощая, можно сказать, что вычислитель читает код как ленту и выполняет выражение за выражением. В Haskell всё совсем по-другому. Мы можем писать функции в любом порядке, также в любом порядке мы можем объявлять локальные переменные в where или let-выражениях. Компилятор определяет порядок редукции синонимов по функциональным зависимостям. Синоним f не будет раскрыт раньше синонима g только в том случае, если результат g требуется в f. Но с обновлением значения этот вариант не пройдёт, посмотрим на выражение:

fun :: Int -> Int
fun arg = 
    let mem = new arg
        x   = read mem
        y   = x + 1
        ??  = write mem y
        z   = read mem
    in z

Предполагается, что в этой функции мы получаем значение arg, выделяем память mem c помощью специальной функции new, которая принимает начальное значение, которое будет храниться в памяти. Затем читаем из памяти, прибавляем к значению единицу, снова записываем в память, потом опять читаем из памяти, сохранив значение в переменной z, и в самом конце возвращаем ответ. Налицо две проблемы: z не зависит от y, поэтому мы можем считать значение z в любой момент после инициализации памяти и вторая проблема: что должна возвращать функция write?

Для того чтобы упорядочить эти вычисления мы воспользуемся типом State. Каждое выражение будет принимать фиктивное состояние и возвращать его. Тогда функция fun запишется так:

fun :: Int -> State s Int
fun arg = State $ \s0 -> 
    let (mem, s1)   = runState (new arg)          s0
        ((),  s2)   = runState (write mem arg)    s1
        (x,   s3)   = runState (read mem)         s2
        y           = x + 1
        ((),  s4)   = runState (write mem y)      s3
        (z,   s5)   = runState (read mem)         s4
    in (z, s5)

new     :: a -> State s (Mem a)
write   :: Mem a -> a -> State s ()
read    :: Mem a -> State s a

Тип Mem параметризован типом значения, которое хранится в памяти. В этом варианте мы не можем изменить порядок следования выражений, поскольку нам приходится передавать состояние. Мы могли бы записать это выражение гораздо короче с помощью методов класса Monad, но мне хотелось подчеркнуть как передача состояния навязывает порядок вычисления. Функция write теперь возвращает пустой кортеж. Но порядок не теряется за счёт состояния. Пустой кортеж намекает на то, что единственное назначение функции write – это обновление состояния.

Однако этого не достаточно. Мы хотим, чтобы обновление значения было скрыто от пользователя в чистой функции. Мы хотим, чтобы тип функции fun не содержал типа State. Для этого нам откуда-то нужно взять начальное значение состояния. Мы можем решить эту проблему, зафиксировав тип s. Пусть это будет тип FakeState, скрытый от пользователя.

module Mutable(
    Mutable, Mem, purge, 
    new, read, write)
where

newtype Mutable a = Mutable (State FakeState a)

data FakeState = FakeState

purge :: Mutable a -> a
purge (Mutable a) = fst $ runState a FakeState

new     :: a -> Mutable (Mem a)
read    :: Mem a -> Mutable a
write   :: Mem a -> a -> Mutable ()

Мы предоставим пользователю лишь тип Mutable без конструктора и функцию purge, которая “очищает” значение от побочных эффектов и примитивные функции для работы с памятью. Также мы определим экземпляры классов типа State для Mutable, сделать это будет совсем не трудно, ведь Mutable – это просто обёртка. С помощью этих экземпляров пользователь сможет комбинировать вычисления, которые связаны с изменением памяти. Пока вроде всё хорошо, но обеспечиваем ли мы локальность изменения значений? Нам важно, чтобы, один раз начав работать с памятью типа Mem, мы не смогли бы нигде воспользоваться этой памятью после выполнения функции purge. Оказывается, что мы можем разрушить локальность. Посмотрите на пример:

let mem = purge allocate
in  purge (read mem)

Мы возвращаем из функции purge ссылку на память и спокойно пользуемся ею в другой ветке Mutable-вычислений. Можно ли этого избежать? Оказывается, что можно. Причём решение весьма элегантно. Мы можем построить типы Mem и Mutable так, чтобы ссылке на память не удалось просочиться через функцию purge. Для этого мы вернёмся к общему типу State c двумя параметрами. Причём первый параметр мы прицепим и к Mem:

data    Mem     s a = ..
newtype Mutable s a = ..

new     :: a -> Mutable s (Mem s a)
write   :: Mem s a -> a -> Mutable s ()
read    :: Mem s a -> Mutable s a

Теперь при создании типы Mem и Mutable связаны общим параметром s. Посмотрим на тип функции purge

purge :: (forall s. Mutable s a) -> a

Она имеет необычный тип. Слово forall означает “для любых”. Это слово называют квантором всеобщности. Этим мы говорим, что функция извлечения значения не может делать никаких предположений о типе фиктивного состояния. Как дополнительный forall может нам помочь? Функция purge забывает тип фиктивного состояния s из типа Mutable, но в случае типа Mem, этот параметр продолжает своё путешествие по программе в типе значения v :: Mem s a. По типу v компилятор может сказать, что существует такое s, для которого значение v имеет смысл (правильно типизировано). Но оно не любое! Функцию purge с трюком интересует не некоторый тип, а все возможные типы s, поэтому пример не пройдёт проверку типов. Компилятор будет следить за “чистотой” наших обновлений.

При таком подходе остаётся вопрос: откуда мы возьмём начальное значение, ведь теперь у нас нет типа FakeState? В Haskell специально для этого типа было сделано исключение. Мы возьмём его из воздуха. Это чисто фиктивный параметр, нам главное, что он скрыт от пользователя, и он нигде не может им воспользоваться. Поскольку у нас нет конструктора Mutable мы никогда не сможем добраться до внутренней функции типа State и извлечь состояние. Состояние скрыто за интерфейсом класса Monad и отбрасывается в функции purge.

Тип ST

Выше я пользовался вымышленными типами для упрощения объяснений, на самом деле в Haskell за обновление значений отвечает тип ST (сокращение от state transformer). Он живёт в модуле Control.Monad.ST. Из документации видно, что у него два параметра, и нет конструкторов:

data ST s a

Это наш тип Mutable, теперь посмотрим на тип Mem. Он называется ST-ссылкой и определён в модуле Data.STRef (сокращение от ST reference). Посмотрим на основные функции:

newSTRef    :: a -> ST s (STRef s a)
readSTRef   :: STRef s a -> ST s a
writeSTRef  :: STRef s a -> a -> ST s ()

Такие функции иногда называют смышлёными конструкторами (smart constructors) они позволяют строить значение, но скрывают от пользователя реализацию за счёт скрытия конструкторов типа (модуль экспортирует лишь имя типа STRef).

Для иллюстрации этих функций реализуем одну вспомогательную функцию из модуля Data.STRef, функцию обновления значения по ссылке:

modifySTRef :: STRef s a -> (a -> a) -> ST s ()
modifySTRef ref f = writeSTRef . f =<< readSTRef ref 

Мы воспользовались тем, что ST является экземпляром Monad. Также как и для State для ST определены экземпляры классов Functor, Applicative и Monad. Какое совпадение! Посмотрим на функцию purge:

runST :: (forall s. ST s a) -> a

Императивные циклы

Реализуем for цикл из языка C:

Result s;

for (i = 0 ; i < n; i++)
    update(i, s);

return s;

У нас есть стартовое значение счётчика и результата, функция обновления счётчика, предикат останова и функция обновления результата. Мы инициализируем счётчик и затем обновляем счётчик и состояние до тех пор пока предикат счётчика не станет ложным. Напишем чистую функцию, которая реализует этот процесс. В этой функции мы воспользуемся специальным синтаксическим сахаром, который называется do-нотация, не пугайтесь это всё ещё Haskell, для понимания этого примера загляните в раздел “сахар для монад” главы~17.

module Loop where

import Control.Monad

import Data.STRef
import Control.Monad.ST

forLoop ::  i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s
forLoop i0 pred next update s0 = runST $ do
    refI <- newSTRef i0
    refS <- newSTRef s0
    iter refI refS
    readSTRef refS
    where iter refI refS = do
            i <- readSTRef refI
            s <- readSTRef refS
            when (pred i) $ do
                writeSTRef refI $ next i
                writeSTRef refS $ update i s
                iter refI refS

Впрочем, код выше можно понять, если читать его как обычный императивный код. Выражения do-блока выполняются последовательно, одно за другим. Сначала мы инициализируем два изменяемых значения: для счётчика цикла и для состояния. Затем в функции iter мы читаем значения и выполняем проверку предиката pred. Функция when – это стандартная функция из модуля Control.Monad. Она проверяет предикат, и если он возвращает True выполняет серию действий, в которых мы записываем обновлённые значения. Обратите внимание на то, что связка when-do это не специальная конструкция языка. Как было сказано when – это просто функция, но она ожидает одно действие, а мы хотим выполнить сразу несколько. Следующее за ней do начинает блок действий (границы блока определяются по отступам), который будет интерпретироваться как одно действие. В настоящем императивном цикле в обновлении и предикате счётчика может участвовать переменная результата, но это считается признаком дурного стиля, поэтому наши функции определены на типе счётчика. Решим типичную задачу, посчитаем числа от одного до десяти:

*Loop> forLoop 1 (<=10) succ (+) 0
55

Посчитаем факториал:

*Loop> forLoop 1 (<=10) succ (*) 1
3628800
*Loop> forLoop 1 (<=100) succ (*) 1
9332621544394415268169923885626670049071596826
4381621468592963895217599993229915608941463976
1565182862536979208272237582511852109168640000
00000000000000000000

Теперь напишем while-цикл:

Result s;

while (pred(s))
    update(s);

return s;

В этом цикле участвует один предикат и одна функция обновления результата, мы обновляем результат до тех пор пока предикат не станет ложным.

whileLoop :: (s -> Bool) -> (s -> s) -> s -> s
whileLoop pred update s0 = runST $ do
    ref <- newSTRef s0
    iter ref 
    readSTRef ref
    where iter ref = do
            s <- readSTRef ref
            when (pred s) $ do
                writeSTRef ref $ update s
                iter ref

Посчитаем сумму чисел через while-цикл:

*Loop> whileLoop ((>0) . fst) (\(n, s) -> (pred n, n + s)) (10, 0)
(0,55)

Первый элемент пары играет роль счётчика, а во втором мы накапливаем результат.

Быстрая сортировка

Реализуем императивный алгоритм быстрой сортировки. Алгоритм быстрой сортировки хорош не только тем, что он работает очень быстро, но и минимальным расходом памяти. Сортировка проводится в самом массиве, с помощью обмена элементов местами. Но для этого нам понадобятся изменяемые массивы. Этот тип определён в модуле Data.Array.ST. В Haskell есть несколько типов изменяемых массивов (как впрочем и неизменяемых), это связано с различными нюансами размещения элементов в массивах, о которых мы пока умолчим. Следующий класс определяет общий интерфейс к различным массивам:

class (HasBounds a, Monad m) => MArray a e m where
    newArray  :: Ix i => (i, i) -> e -> m (a i e)
    newArray_ :: Ix i => (i, i) -> m (a i e)

MArray – это сокращение от mutable (изменяемый) array. Метод newArray создаёт массив типа a, который завёрнут в тип-монаду m. Первый аргумент указывает на диапазон значений индексов массива, а вторым аргументом передаётся элемент, который будет записан во все ячейки массива. Вторая функция записывает в массив элемент undefined.

Посмотрим на вспомогательные классы:

class Ord a => Ix a where
    range :: (a, a) -> [a]
    index :: (a, a) -> a -> Int
    inRange :: (a, a) -> a -> Bool
    rangeSize :: (a, a) -> Int

class HasBounds a where
    bounds :: Ix i => a i e -> (i, i)

Класс Ix описывает тип индекса из непрерывного диапазона значений. Наверняка по имени функции и типу вы догадаетесь о назначении методов (можете свериться с интерпретатором на типах Int или (Int, Int)). Класс HasBounds обозначает массивы размер, которых фиксирован. Но вернёмся к массивам. Мы можем не только выделять память под массив, но и читать элементы и обновлять их:

readArray  :: (MArray a e m, Ix i) => a i e -> i -> m e
writeArray :: (MArray a e m, Ix i) => a i e -> i -> e -> m ()

В случае ST-ссылок у нас была функция runST. Она возвращала значение из памяти, но что будет возвращать аналогичная функция для массива? Посмотрим на неё:

freeze :: (Ix i, MArray a e m, IArray b e) => a i e -> m (b i e)

Возможно за всеми классами схожесть с функцией runST прослеживается не так чётко. Новый класс IArray обозначает неизменяемые (immutable) массивы. Функцией freeze мы превращаем изменяемый массив в неизменяемый, но завёрнутый в специальный тип-монаду. В нашем случае этим типом будет ST. В модуле Data.Array.ST определена специальная версия этой функции:

runSTArray :: Ix i => (forall s . ST s (STArray s i e)) -> Array i e

Здесь Array – это обычный неизменяемый массив. Он живёт в модуле Data.Array мы можем строить массивы из списков значений, преобразовывать их разными способами, превращать в обратно в списки и многое другое. Об о всём этом можно узнать из документации к модулю. Обратите на появление слова forall и в этой функции. Оно несёт тот же смысл, что и в функции runST.

Для тренировки напишем функцию, которая меняет местами два элемента массива:

module Qsort where

import Data.STRef
import Control.Monad.ST

import Data.Array
import Data.Array.ST
import Data.Array.MArray

swapElems :: Ix i => i -> i -> STArray s i e -> ST s ()
swapElems i j arr = do
     vi <- readArray arr i
     vj <- readArray arr j

     writeArray arr i vj
     writeArray arr j vi

Протестируем на небольшом массиве:

test :: Int -> Int -> [a] -> [a]
test i j xs = elems $ runSTArray $ do
    arr <- newListArray (0, length xs - 1) xs
    swapElems i j arr
    return arr

Тир функции test ничем не выдаёт её содержание. Вроде функция как функция:

test :: Int -> Int -> [a] -> [a]

Посмотрим на то, как она работает:

*Qsort> test 0 3 [0,1,2,3,4]
[3,1,2,0,4]
*Qsort> test 0 4 [0,1,2,3,4]
[4,1,2,3,0]

Теперь перейдём к сортировке. Суть метода в том, что мы выбираем один элемент массива, называемый осью (pivot) и переставляем остальные элементы массива так, чтобы все элементы меньше осевого были слева от него, а все, что больше оказались справа. Затем мы повторяем эту процедуру на массивах поменьше, тех, что находятся слева и справа от осевого элемента и так пока все элементы не отсортируются. В алгоритме очень хитрая процедура перестановки элементов, наша задача переставить элементы в массиве, то есть не пользуясь никакими дополнительными структурами данных. Я не буду говорить как это делается, просто выпишу код, а вы можете почитать об этом где-нибудь, в любом случае из кода будет понятно как это происходит:

qsort :: Ord a => [a] -> [a]
qsort xs = elems $ runSTArray $ do
    arr <- newListArray (left, right) xs
    qsortST left right arr
    return arr
    where left  = 0
          right = length xs - 1
 
qsortST :: Ord a => Int -> Int -> STArray s Int a -> ST s ()
qsortST left right arr = do
    when (left <= right) $ do
        swapArray left (div (left + right) 2) arr
        vLeft <- readArray arr left 
        (last, _) <- forLoop (left + 1) (<= right) succ 
                            (update vLeft) (return (left, arr))
        swapArray left last arr
        qsortST left (last - 1) arr
        qsortST (last + 1) right arr
    where update vLeft i st = do
            (last, arr) <- st
            vi <- readArray arr i
            if (vi < vLeft) 
                then do
                    swapArray (succ last) i arr
                    return (succ last, arr)
                else do
                    return (last, arr)

Это далеко не самый быстрый вариант быстрой сортировки, но самый простой. Мы просто учимся обращаться с изменяемыми массивами. Протестируем:

*Qsort> qsort "abracadabra"
"aaaaabbcdrr"
*Qsort> let x = 1000000
*Qsort> last $ qsort [x, pred x .. 0]
-- двадцать лет спустя
1000000

Краткое содержание

Мы посмотрели на примерах как применяются типы State, Reader и Writer. Также мы познакомились с монадой изменяемых значений ST. Она позволяет писать в императивном стиле на Haskell. Мы узнали два новых элемента построения типов:

Также мы узнали несколько полезных типов:

Отметим, что экземпляр класса Monad определён и для функций. Мы можем записать функцию двух аргументов (a -> b -> c) как (a -> (->) b c). Тогда тип (->) b будет типом с одним параметром, как раз то, что нужно для класса Monad. По смыслу экземпляр класса Monad для функций совпадает с экземпляром типа Reader. Первый аргумент стрелочного типа b играет роль окружения.

Упражнения

Зарегистрировано под лицензией Creative commons Attribution-NonCommercial-NoDerivs 3.0 Generic (CC BY-NC-ND 3.0)