Заглавная страница Избранные статьи Случайная статья Познавательные статьи Новые добавления Обратная связь FAQ Написать работу КАТЕГОРИИ: АрхеологияБиология Генетика География Информатика История Логика Маркетинг Математика Менеджмент Механика Педагогика Религия Социология Технологии Физика Философия Финансы Химия Экология ТОП 10 на сайте Приготовление дезинфицирующих растворов различной концентрацииТехника нижней прямой подачи мяча. Франко-прусская война (причины и последствия) Организация работы процедурного кабинета Смысловое и механическое запоминание, их место и роль в усвоении знаний Коммуникативные барьеры и пути их преодоления Обработка изделий медицинского назначения многократного применения Образцы текста публицистического стиля Четыре типа изменения баланса Задачи с ответами для Всероссийской олимпиады по праву Мы поможем в написании ваших работ! ЗНАЕТЕ ЛИ ВЫ?
Влияние общества на человека
Приготовление дезинфицирующих растворов различной концентрации Практические работы по географии для 6 класса Организация работы процедурного кабинета Изменения в неживой природе осенью Уборка процедурного кабинета Сольфеджио. Все правила по сольфеджио Балочные системы. Определение реакций опор и моментов защемления |
Вкусные вычисления с состояниемСодержание книги
Поиск на нашем сайте
Haskell является чистым языком, и вследствие этого наши програм- мы состоят из функций, которые не могут изменять какое бы то ни было глобальное состояние или переменные – они могут только производить какие-либо вычисления и возвращать результаты. На самом деле данное огра- ничение упрощает задачу обдумывания наших про- грамм, освобождая нас от необходимости заботить- ся о том, какое значение имеет каждая переменная в определённый момент времени. Тем не менее некото- рые задачи по своей при- роде обладают состоя- нием, поскольку зависят от какого-то состояния, изменяющегося с течени- ем времени. Хотя это не проблема для Haskell, такие вычисления могут быть немного уто- мительными для моделирования. Вот почему в языке Haskell есть монада State, благодаря которой решение задач с внутренним со- стоянием становится сущим пустяком – и в то же время остаётся красивым и чистым. Когда мы разбирались со случайными числами в главе 9, то имели дело с функциями, которые в качестве параметра прини- мали генератор случайных чисел и возвращали случайное число и новый генератор случайных чисел. Если мы хотели сгенери- ровать несколько случайных чисел, нам всегда приходилось ис- пользовать генератор случайных чисел, который возвращала предыдущая функция вместе со своим результатом. Например, чтобы создать функцию, которая принимает значение типа StdGen и трижды «подбрасывает монету» на основе этого генератора, мы делали следующее: threeCoins:: StdGen –> (Bool, Bool, Bool) threeCoins gen =
let (firstCoin, newGen) = random gen (secondCoin, newGen') = random newGen (thirdCoin, newGen'') = random newGen' in (firstCoin, secondCoin, thirdCoin) Эта функция принимает генератор gen, а затем вызов random gen возвращает значение типа Bool наряду с новым генератором. Для подбрасывания второй монеты мы используем новый генера- тор, и т. д. В большинстве других языков нам не нужно было бы возвра- щать новый генератор вместе со случайным числом. Мы могли бы просто изменить имеющийся! Но поскольку Haskell является чис- тым языком, этого сделать нельзя, поэтому мы должны были взять какое-то состояние, создать из него результат и новое состояние, а затем использовать это новое состояние для генерации новых ре- зультатов. Можно подумать, что для того, чтобы не иметь дела с вычисле- ниями с состоянием вручную подобным образом, мы должны были бы отказаться от чистоты языка Haskell. К счастью, такую жертву приносить не нужно, так как существует специальная небольшая мо- нада под названием State. Она превосходно справляется со всеми этими делами с состоянием, никоим образом не влияя на чистоту, благодаря которой программирование на языке Haskell настолько оригинально и изящно.
Вычисления с состоянием Чтобы лучше продемонстрировать вычисления с внутренним со- стоянием, давайте просто возьмём и дадим им тип. Мы скажем, что вычисление с состоянием – это функция, которая принимает некое состояние и возвращает значение вместе с неким новым состояни- ем. Данная функция имеет следующий тип: s –> (a, s) Идентификатор s обозначает тип состояния; a – это результат вычислений с состоянием. ПРИМЕЧАНИЕ. В большинстве других языков присваивание зна- чения может рассматриваться как вычисление с состоянием. На- пример, когда мы выполняем выражение x = 5 в императивном языке, как правило, это присваивает переменной x значение 5, и в нём также в качестве выражения будет фигурировать значе- ние 5. Если рассмотреть это действие с функциональной точки зрения, получается нечто вроде функции, принимающей состо- яние (то есть все переменные, которым ранее были присвоены значения) и возвращающей результат (в данном случае 5) и но- вое состояние, которое представляло бы собой все предшеству- ющие соответствия переменных значениям плюс переменную с недавно присвоенным значением. Это вычисление с состоянием – функцию, которая принимает состояние и возвращает результат и новое состояние – также мож- но воспринимать как значение с контекстом. Действительным зна- чением является результат, тогда как контекстом является то, что мы должны предоставить некое исходное состояние, чтобы фак- тически получить этот результат, и то, что помимо результата мы также получаем новое состояние.
Стеки и чебуреки Предположим, мы хотим смоделировать стек. Стек – это структура данных, которая содержит набор элементов и поддерживает ровно две операции: ® проталкивание элемента в стек (добавляет элемент на вер- хушку стека); ® выталкивание элемента из стека (удаляет самый верхний элемент из стека). Для представления нашего стека будем использовать список, «голова» которого действует как вершина стека. Чтобы решить эту задачу, создадим две функции: ® функция pop будет принимать стек, выталкивать один эле- мент и возвращать его в качестве результата. Кроме того, она возвращает новый стек без вытолкнутого элемента; ® функция push будет принимать элемент и стек, а затем про- талкивать этот элемент в стек. В качестве результата она бу- дет возвращать значение () вместе с новым стеком. Вот используемые функции: type Stack = [Int] pop:: Stack –> (Int, Stack)
pop (x:xs) = (x, xs) push:: Int –> Stack –> ((), Stack) push a xs = ((), a:xs) При проталкивании в стек в качестве результата мы использо- вали значение (), поскольку проталкивание элемента на вершину стека не несёт какого-либо существенного результирующего зна- чения – его основная задача заключается в изменении стека. Если мы применим только первый параметр функции push, мы получим вычисление с состоянием. Функция pop уже является вычислением с состоянием вследствие своего типа. Давайте напишем небольшой кусок кода для симуляции стека, используя эти функции. Мы возьмём стек, протолкнем в него зна- чение 3, а затем вытолкнем два элемента просто ради забавы. Вот оно: stackManip:: Stack –> (Int, Stack) stackManip stack = let ((), newStack1) = push 3 stack (a, newStack2) = pop newStack1 in pop newStack2 Мы принимаем стек, а затем выполняем выражение push 3 stack, что даёт в результате кортеж. Первой частью кортежа является зна- чение (), а второй частью является новый стек, который мы назы- ваем newStack1. Затем мы выталкиваем число из newStack1, что даёт в результате число a (равно 3), которое мы протолкнули, и новый стек, названный нами newStack2. Затем мы выталкиваем число из newStack2 и получаем число и новый стек. Мы возвращаем кортеж с этим числом и новым стеком. Давайте попробуем: ghci> stackManip [5,8,2,1] (5,[8,2,1]) Результат равен 5, а новый стек – [8,2,1]. Обратите внимание, как функция stackManip сама является вычислением с состоянием. Мы взяли несколько вычислений с состоянием и как бы «склеили» их вместе. Хм-м, звучит знакомо. Предшествующий код функции stackManip несколько громоздок, потому как мы вручную передаём состояние каждому вычислению
с состоянием, сохраняем его, а затем передаём следующему. Не луч- ше ли было бы, если б вместо того, чтобы передавать стек каждой функции вручную, мы написали что-то вроде следующего: stackManip = do push 3 a <– pop pop Ла-адно, монада State позволит нам делать именно это!.. С её помощью мы сможем брать вычисления с состоянием, подобные этим, и использовать их без необходимости управлять состоянием вручную.
Монада State Модуль Control.Monad.State предоставляет тип newtype, который оборачивает вычисления с состоянием. Вот его определение: newtype State s a = State { runState:: s –> (a, s) } Тип State s a – это тип вычисления с состоянием, которое мани- пулирует состоянием типа s и имеет результат типа a. Как и модуль Control.Monad.Writer, модуль Control.Monad.State не экспортирует свой конструктор значения. Если вы хотите взять вычисление с состоянием и обернуть его в newtype State, используй- те функцию state, которая делает то же самое, что делал бы конс- труктор State. Теперь, когда вы увидели, в чём заключается суть вычислений с состоянием и как их можно даже воспринимать в виде значений с контекстами, давайте рассмотрим их экземпляр класса Monad: instance Monad (State s) where return x = State $ \s –> (x, s) (State h) >>= f = State $ \s –> let (a, newState) = h s (State g) = f a in g newState Наша цель использования функции return состоит в том, что- бы взять значение и создать вычисление с состоянием, которое всегда содержит это значение в качестве своего результата. По- этому мы просто создаём анонимную функцию \s –> (x, s). Мы
всегда представляем значение x в качестве результата вычисления с состоянием, а состояние остаётся неизменным, так как функция return должна помещать значение в минимальный контекст. По- тому функция return создаст вычисление с состоянием, которое представляет определённое значение в качестве результата, а со- стояние сохраняет неизменным. А что насчёт операции >>=? Ну что ж, результатом передачи вы- числения с состоянием функции с помощью операции >>= должно быть вычисление с состоянием, верно? Поэтому мы начинаем с обёртки newtype State, а затем вызываем анонимную функцию. Эта анонимная функция будет нашим новым вычислением с со- стоянием. Но что же в ней про- исходит? Нам каким-то образом нужно извлечь значение резуль- тата из первого вычисления с со- стоянием. Поскольку прямо сей- час мы находимся в вычислении с состоянием, то можем передать вычислению с состоянием h наше текущее состояние s, что в результате даёт пару из результата и но- вого состояния: (a, newState). До сих пор каждый раз, когда мы реализовывали операцию >>=, сразу же после извлечения результата из монадического значения мы применяли к нему функцию f, чтобы получить новое монади- ческое значение. В случае с монадой Writer после того, как это сде- лано и получено новое монадическое значение, нам по-прежнему нужно позаботиться о контексте, объединив прежнее и новое мо- ноидные значения с помощью функции mappend. Здесь мы выпол- няем вызов выражения f a и получаем новое вычисление с состо- янием g. Теперь, когда у нас есть новое вычисление с состоянием и новое состояние (известное под именем newState), мы просто применяем это вычисление с состоянием g к newState. Результатом является кортеж из окончательного результата и окончательного состояния! Итак, при использовании операции >>= мы как бы «склеиваем» друг с другом два вычисления, обладающих состоянием. Второе вычисление скрыто внутри функции, которая принимает результат
предыдущего вычисления. Поскольку функции pop и push уже яв- ляются вычислениями с состоянием, легко обернуть их в обёртку State: import Control.Monad.State pop:: State Stack Int pop = state $ \(x:xs) –> (x, xs) push:: Int –> State Stack () push a = state $ \xs –> ((), a:xs) Обратите внимание, как мы задействовали функцию state, чтобы обернуть функцию в конструктор newtype State, не прибегая к использованию конструктора значения State напрямую. Функция pop – уже вычисление с состоянием, а функция push принимает значение типа Int и возвращает вычисление с состоя- нием. Теперь мы можем переписать наш предыдущий пример про- талкивания числа 3 в стек и выталкивания двух чисел подобным образом: import Control.Monad.State
stackManip:: State Stack Int stackManip = do push 3 a <– pop pop Видите, как мы «склеили» проталкивание и два выталкивания в одно вычисление с состоянием? Разворачивая его из обёртки newtype, мы получаем функцию, которой можем предоставить не- кое исходное состояние: ghci> runState stackManip [5,8,2,1] (5,[8,2,1]) Нам не требовалось привязывать второй вызов функции pop к образцу a, потому что мы вовсе не использовали этот образец. Значит, это можно было записать вот так: stackManip:: State Stack Int stackManip = do
push 3 pop pop Очень круто! Но что если мы хотим сделать что-нибудь послож- нее? Скажем, вытолкнуть из стека одно число, и если это число рав- но 5, просто протолкнуть его обратно в стек и остановиться. Но если число не равно 5, вместо этого протолкнуть обратно 3 и 8. Вот он код: stackStuff:: State Stack () stackStuff = do a <– pop if a == 5 then push 5 else do push 3 push 8 Довольно простое решение. Давайте выполним этот код с ис- ходным стеком: ghci> runState stackStuff [9,0,2,1,0] ((),[8,3,0,2,1,0]) Вспомните, что выражения do возвращают в результате монади- ческие значения, и при использовании монады State одно выраже- ние do является также функцией с состоянием. Поскольку функции stackManip и stackStuff являются обычными вычислениями с состо- янием, мы можем «склеивать» их вместе, чтобы производить даль- нейшие вычисления с состоянием: moreStack:: State Stack () moreStack = do a <– stackManip if a == 100 then stackStuff else return () Если результат функции stackManip при использовании текуще- го стека равен 100, мы вызываем функцию stackStuff; в противном случае ничего не делаем. Вызов return () просто сохраняет состоя- ние как есть и ничего не делает.
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Последнее изменение этой страницы: 2017-02-17; просмотров: 182; Нарушение авторского права страницы; Мы поможем в написании вашей работы! infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 18.227.140.152 (0.007 с.) |