проблемы в модуле 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) where
mempty
= Sum 0
mappend (Sum a) (Sum b) = Sum (a + b)
А другой для Prod:
instance Num a => Monoid (Prod a) where
mempty
= Prod 1
mappend (Prod a) (Prod b) = Prod (a * b)
Записи
Вторая новинка заключалась в фигурных скобках. С помощью фигурных скобок в 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)
В фигурных скобках через запятую мы указываем поля. Поле состоит из имени и типа. Теперь нам до-
ступны две операции:
• Чтение полей
hello :: Passport -> String
hello p = ”Hello, ” ++ givenName p ++ ”!”
114 | Глава 7: Функторы и монады: примеры
Для чтения мы просто подставляем в имя поля данное значение. В этой функции мы приветствуем
человека и обращаемся к нему по имени. Для того, чтобы узнать его имя мы подсмотрели в паспорт, в
поле givenName.
• Обновление полей. Для обновления полей мы пользуемся таким синтаксисом:
value { fieldName1 = newValue1, fieldName2 = newValue2, … }
Мы присваиваем в значении value полю с именем fieldName новое значение newFieldValue. К примеру
продлим срок действия паспорта на десять лет:
prolongate :: Passport -> Passport
prolongate p = p{ dateOfExpiry = newDate }
where newDate = oldDate { year = year oldDate + 10 }
oldDate = dateOfExpiry p
Вернёмся к типам 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, которая записывает сообщение в накопитель и