haskell-notes

Теперь мы можем определить функцию 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

112 | Глава 7: Функторы и монады: примеры

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

Рассмотрим по-подробнее тип 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. Её видит лишь компилятор. Это означает, что на разворачивание и заворачивание такого значения

в тип обёртку не тратится никаких усилий. Такие типы подходят для решения двух задач:

• Более точная проверка типов.

Например у нас есть типы, которые описывают физические величины, все они являются числами, но у

них также есть и размерности. Мы можем написать:

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

Читай продолжение на следующей странице
Добавить комментарии