haskell-notes

проблемы в модуле 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, которая записывает сообщение в накопитель и

Страницы: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162