`
Читать книги » Книги » Компьютеры и Интернет » Программирование » Миран Липовача - Изучай Haskell во имя добра!

Миран Липовача - Изучай Haskell во имя добра!

1 ... 78 79 80 81 82 ... 96 ВПЕРЕД
Перейти на страницу:

isBigGang :: Int –> Bool

isBigGang x = x > 9

Ну а что если теперь вместо возвращения значения True или False мы хотим, чтобы функция также возвращала строку журнала, которая сообщает, что она сделала? Что ж, мы просто создаём эту строку и возвращаем её наряду с нашим значением Bool:

isBigGang :: Int –> (Bool, String)

isBigGang x = (x > 9, "Размер банды сравнён с 9.")

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

ghci> isBigGang 3

(False,"Размер банды сравнён с 9.")

ghci> isBigGang 30

(True,"Размер банды сравнён с 9.")

Пока всё нормально. Функция isBigGang принимает нормальное значение и возвращает значение с контекстом. Как мы только что увидели, передача ей нормального значения не составляет сложности. Теперь предположим, что у нас уже есть значение, у которого имеется журнальная запись, присоединённая к нему – такая как (3, "Небольшая банда.") – и мы хотим передать его функции isBigGang. Похоже, перед нами снова встаёт вопрос: если у нас есть функция, которая принимает нормальное значение и возвращает значение с контекстом, как нам взять нормальное значение с контекстом и передать его функции?

Исследуя монаду Maybe, мы создали функцию applyMaybe, которая принимала значение типа Maybe a и функцию типа a –> Maybe b и передавала это значение Maybe a в функцию, даже если функция принимает нормальное значение типа a вместо Maybe a. Она делала это, следя за контекстом, имеющимся у значений типа Maybe a, который означает, что эти значения могут быть значениями с неуспехом вычислений. Но внутри функции типа a –> Maybe b мы могли обрабатывать это значение как нормальное, потому что applyMaybe (которая позже стала функцией >>=) проверяла, являлось ли оно значением Nothing либо значением Just.

В том же духе давайте создадим функцию, которая принимает значение с присоединённым журналом, то есть значением типа (a,String), и функцию типа a –> (b,String), и передаёт это значение в функцию. Мы назовём её applyLog. Однако поскольку значение типа (a,String) не несёт с собой контекст возможной неудачи, но несёт контекст добавочного значения журнала, функция applyLog будет обеспечивать сохранность первоначального значения журнала, объединяя его со значением журнала, возвращаемого функцией. Вот реализация этой функции:

applyLog :: (a,String) –> (a –> (b,String)) –> (b,String)

applyLog (x,log) f = let (y,newLog) = f x in (y,log ++ newLog)

Когда у нас есть значение с контекстом и мы хотим передать его функции, то мы обычно пытаемся отделить фактическое значение от контекста, затем пытаемся применить функцию к этому значению, а потом смотрим, сбережён ли контекст. В монаде Maybe мы проверяли, было ли значение равно Just x, и если было, мы брали это значение x и применяли к нему функцию. В данном случае очень просто определить фактическое значение, потому что мы имеем дело с парой, где один компонент является значением, а второй – журналом. Так что сначала мы просто берём значение, то есть x, и применяем к нему функцию f. Мы получаем пару (y,newLog), где y является новым результатом, а newLog – новым журналом. Но если мы вернули это в качестве результата, прежнее значение журнала не было бы включено в результат, так что мы возвращаем пару (y,log ++ newLog). Мы используем операцию конкатенации ++, чтобы добавить новый журнал к прежнему.

Вот функция applyLog в действии:

ghci> (3, "Небольшая банда.") `applyLog` isBigGang

(False,"Небольшая банда.Размер банды сравнён с 9.")

ghci> (30, "Бешеный взвод.") `applyLog` isBigGang

(True,"Бешеный взвод.Размер банды сравнён с 9.")

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

Вот ещё несколько примеров использования applyLog:

ghci> ("Тобин","Вне закона.") `applyLog` (x –> (length x "Длина."))

(5,"Вне закона.Длина.")

ghci> ("Котопёс","Вне закона.") `applyLog` (x –> (length x "Длина."))

(7,"Вне закона.Длина.")

Смотрите, как внутри анонимной функции образец x является просто нормальной строкой, а не кортежем, и как функция applyLog заботится о добавлении записей журнала.

Моноиды приходят на помощь

Убедитесь, что вы на данный момент знаете, что такое моноиды!

Прямо сейчас функция applyLog принимает значения типа (a,String), но есть ли смысл в том, чтобы тип журнала был String? Он использует операцию ++ для добавления записей журнала – не будет ли это работать и в отношении любого типа списков, не только списка символов? Конечно же, будет! Мы можем пойти дальше и изменить тип этой функции на следующий:

applyLog :: (a,[c]) –> (a –> (b,[c])) –> (b,[c])

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

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

ghci> [1,2,3] `mappend` [4,5,6]

[1,2,3,4,5,6]

ghci> B.pack [99,104,105] `mappend` B.pack [104,117,97,104,117,97]

Chunk "chi" (Chunk "huahua" Empty)

Круто! Теперь наша функция applyLog может работать для любого моноида. Мы должны изменить тип, чтобы отразить это, а также реализацию, потому что следует заменить вызов операции ++ вызовом функции mappend:

applyLog :: (Monoid m) => (a,m) –> (a –> (b,m)) –> (b,m)

applyLog (x,log) f = let (y,newLog) = f x

                     in (y,log `mappend` newLog)

Поскольку сопутствующее значение теперь может быть любым моноидным значением, нам больше не нужно думать о кортеже как о значении и журнале, но мы можем думать о нём как о значении с сопутствующим моноидным значением. Например, у нас может быть кортеж, в котором есть имя предмета и цена предмета в виде моноидного значения. Мы просто используем определение типа newtype Sum, чтобы быть уверенными, что цены добавляются, пока мы работаем с предметами. Вот функция, которая добавляет напиток к обеду какого-то ковбоя:

import Data.Monoid

type Food = String

type Price = Sum Int

addDrink :: Food –> (Food,Price)

addDrink "бобы" = ("молоко", Sum 25)

addDrink "вяленое мясо" = ("виски", Sum 99)

addDrink _ = ("пиво", Sum 30)

Мы используем строки для представления продуктов и тип Int в обёртке типа newtype Sum для отслеживания того, сколько центов стоит тот или иной продукт. Просто напомню: выполнение функции mappend для значений типа Sum возвращает сумму обёрнутых значений.

ghci> Sum 3 `mappend` Sum 9

Sum {getSum = 12}

Функция addDrink довольно проста. Если мы едим бобы, она возвращает "молоко" вместе с Sum 25; таким образом, 25 центов завёрнуты в конструктор Sum. Если мы едим вяленое мясо, то пьём виски, а если едим что-то другое – пьём пиво. Обычное применение этой функции к продукту сейчас было бы не слишком интересно, а вот использование функции applyLog для передачи продукта с указанием цены в саму функцию представляет интерес:

ghci> ("бобы", Sum 10) `applyLog` addDrink

("молоко",Sum {getSum = 35})

ghci> ("вяленое мясо", Sum 25) `applyLog` addDrink

("виски",Sum {getSum = 124})

ghci> ("собачатина", Sum 5) `applyLog` addDrink

("пиво",Sum {getSum = 35})

Молоко стоит 25 центов, но если мы заедаем его бобами за 10 центов, это обходится нам в 35 центов. Теперь ясно, почему присоединённое значение не всегда должно быть журналом – оно может быть любым моноидным значением, и то, как эти два значения объединяются, зависит от моноида. Когда мы производили записи в журнал, они присоединялись в конец, но теперь происходит сложение чисел.

Поскольку значение, возвращаемое функцией addDrink, является кортежем типа (Food,Price), мы можем передать этот результат функции addDrink ещё раз, чтобы функция сообщила нам, какой напиток будет подан в сопровождение к блюду и сколько это нам будет стоить. Давайте попробуем:

1 ... 78 79 80 81 82 ... 96 ВПЕРЕД
Перейти на страницу:

Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Миран Липовача - Изучай Haskell во имя добра!, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

Комментарии (0)