Обычно функции из модуля Data.Map
включаются с директивой qualified, поскольку имена многихфункций из этого модуля совпадают с именами из модуля Prelude
. Теперь все определения из модуляData.Map
пишутся с приставкой M. .Создадим вспомогательную функцию, которая упростит вычисление выражений:
runExp :: Exp ->
[(String, Int)] -> IntrunExp 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 ofLit
n->
nNeg
n->
negate $ eval’ nAdd
a b->
eval’ a + eval’ bMul
a b->
eval’ a + eval’ bVar
name->
value env namewhere
eval’ = eval env112 | Глава 7: Функторы и монады: примеры
7.4 Накопление результата
Рассмотрим по-подробнее тип Writer
. Он выполняет задачу обратную к типу Reader. Когда мы пользова-лись типом Reader
, мы могли в любом месте функции извлекать данные из окружения. Теперь же мы будемне извлекать данные из окружения, а записывать их.
Рассмотрим такую задачу нам нужно обойти дерево типа Exp
и подсчитать все бинарные операции. Мыприбавляем к накопителю результата единицу за каждый конструктор Add
или Mul. Тип сообщений будетчислом. Нам нужно сделать экземпляр класса Monoid
для чисел.Напомню, что тип накопителя должен быть экземпляром класса Monoid
:class Monoid
a wheremempty
::
amappend ::
a -> a -> amconcat ::
[a] -> amconcat =
foldr mappend memptyНо для чисел возможно несколько вариантов, которые удовлетворяют свойствам. Для сложения:
instance Num
a => Monoid a wheremempty
=
0mappend =
(+)И умножения:
instance Num
a => Monoid a wheremempty
=
1mappend =
(*)Для нашей задачи подойдёт первый вариант, но не исключена возможность того, что для другой зада-
чи нам понадобится второй. Но тогда мы уже не сможем определить такой экземпляр. Для решения этой
проблемы в модуле 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. Её видит лишь компилятор. Это означает, что на разворачивание и заворачивание такого значенияв тип обёртку не тратится никаких усилий. Такие типы подходят для решения двух задач:
• Более точная проверка типов.
Например у нас есть типы, которые описывают физические величины, все они являются числами, но у
них также есть и размерности. Мы можем написать:
type Velocity
= Double
type Time
= Double
type Length
= Double
velocity :: Length -> Time -> Velocity
velocity leng time =
leng / timeНакопление результата | 113
В этом случае мы спокойно можем подставить на место времени путь и наоборот. Но с помощью типов
обёрток мы можем исключить эти случаи:
newtype Velocity
= Velocity
Double
newtype Time
= Time
Double
newtype Length
= Length
Double
velocity :: Length -> Time -> Velocity
velocity (Length
leng) (Time time) = Velocity $ leng / timeВ этом случае мы проводим проверку по размерностям, компилятор не допустит смешивания данных.
• Определение нескольких экземпляров одного класса для одного типа. Этот случай мы как раз и рас-
сматриваем для класса Monoid
. Нам нужно сделать два экземпляра для одного и того же типа Num a=>
a.Сделаем две обёртки!
newtype Sum
a = Sum
a
newtype Prod
a = Prod aТогда мы можем определить два экземпляра для двух разных типов:
Один для Sum
:instance Num
a => Monoid (Sum a) wheremempty
= Sum
0mappend (Sum
a) (Sum b) = Sum (a + b)А другой для Prod
:instance Num
a => Monoid (Prod a) wheremempty
= Prod
1mappend (Prod
a) (Prod b) = Prod (a * b)Записи
Вторая новинка заключалась в фигурных скобках. С помощью фигурных скобок в Haskell обозначаются