Монады Reader и Writer
В части 3 этой серии, мы наконец затронули идею монад. Мы изучили что они такое, и увидели как некоторые общие типы, например IO
и Maybe
, работают в качестве монад. В этой части, мы посмотрим на некоторые другие полезные монады. В частности мы рассмотрим монады Reader
и Writer
.
Глобальные переменные(или их нехватка)
В Haskell, наш код в общем "чистый", что значит, что функции могут только взаимодействовать с аргументами переданными им. Смысл в том, чтобы мы не могли имметь глобальных переменных. Мы можем ипеть глобальные выражения, но они фиксируются во время компиляции. Если поведение пользователя может изменить их, нам нужно обернуть их в IO
монаду, что значит, что мы не можем использовать её в "чистом" коде.
Представим следующий пример. Мы хотим иметь Environment
содержащее параметры в качестве глобальных переменных. Однако, мы должны их загрузить через конфигурационный файл или командную строку, что трубует IO
монаду.
main1 :: IO ()
main1 = do
env <- loadEnv
let str = func1 env
print str
data Environment = Environment
{ param1 :: String
, param2 :: String
, param3 :: String
}
loadEnv :: IO Environment
loadEnv = ...
func1 :: Environment -> String
func1 env = "Result: " ++ (show (func2 env))
func2 :: Environment -> Int
func2 env = 2 + floor (func3 env)
func3 :: Environment -> Float
func3 env = (fromIntegral $ l1 + l2 + l3) * 2.1
where
l1 = length (param1 env)
l2 = length (param2 env) * 2
l3 = length (param3 env) * 3
Функция на самом деле используется func3
. Однако func3
чистая функцияю. Это значит, она не может вызывать напрямую loadenv
, так как она не "чистая" функция. Это значит, что окружение должно быть передано через переменную в другую функцию, чтобы можно было передать её в функцию func3
. В языке с глобальными переменными, мы должны сохранить env
в качестве глобальной переменой в main
. Функция func3
должна иметь доступ напрямую. Не нужно иметь парметра для func1
и func2
. В больших программах эта передача переменных может устроить головную боль.
Решение READER
Монада Reader
решает эту проблему. Она создает глобальное только для чтения значение определенного типа. Все функции внутри монады могут прочитать "тип". Давайте взглянем на то как монада Reader
меняет форму нашего кода. Наши функции больше не трубуют Environment
в качесте обязательного параметра, так как они могут получить доступ к ней через монаду.
main :: IO ()
main = do
env <- loadEnv
let str = runReader func1' env
print str
func1' :: Reader Environment String
func1' = do
res <- func2'
return ("Result: " ++ show res)
func2' :: Reader Environment Int
func2' = do
env <- ask
let res3 = func3 env
return (2 + floor res3)
-- as above
func3 :: Environment -> Float
...
Функция ask
развертывает окружение для того, чтобы мы могли его исопльзовать. Привязывание действий к моанадам позволяет нам связать различные Reader
действия. Для того, чтобы вызвать действие чтения из чистого кода, нужно вызвать runReader
функцию и подать окружение в качестве параметра. Все функции внутри действия будут обращаться как к глобальной переменной.
Код выше так же вводит важное понятие. Каждый раз, когда вы вводите понятие монада "X", всегда есть соответстующая функция "runX", которая говорит вам как запустить операции над монадой из чистого контекста(IO исключение). Эта функция будет часто требоваться при определенном вводе, так же как и сами вычисления. Затем оно будет производить вывод этим самых вычислений. В этом случае Reader
, у нас есть runReader
функция. Она требует значение, которое мы будем читать, и сами вычисления Reader
.
runReader :: Reader r a -> r -> a
Может быть не похоже, что нам многое удалось, но наш код более понятен теперь. Мы сохранили func3
, так как она есть. Она имеет смысл, чтобы описать её в качестве переменной из Environment
с помощью функции. Однако, наши другие две функции больше не принимают окружение как обязательные параметры. Они просто существуют в контексте где окружение - глобальная переменная.
Сбор значений
Чтобы понять монаду Winter
, давайте поговорим о проблеме сбора. Предположим у нас есть несколько различных функций. Каждая делает строковые операции, которые чего-то стоят. Мы хотим отслеживать сколько "стоят" все вычисления вместе. Мы можем сделать следующее, для сбора аргументов и слежения за "ценой" которую мы получим. Мы продолжаем передавать сохбранные переменные вместе с результатом обработки сть
Now, to motivate the Writer monad, let's talk about the accumulation problem. Suppose we have a few different functions. Each will perform some string operations we've assigned an arbitrary "cost" to. We want to keep track of how "expensive" it was to run the full computation. We can do this by using accumulator arguments to keep track of the cost we've seen so far. We then keep passing the accumulated value along with the final String result.роки.
-- Calls func2 if even length, func3 and func4 if odd
func1 :: String -> (Int, String)
func1 input = if length input `mod` 2 == 0
then func2 (0, input)
else (i1 + i2, str1 ++ str2)
where
(i1, str1) = func3 (0, tail input)
(i2, str2) = func4 (0, take 1 input)
-- Calls func4 on truncated version
func2 :: (Int, String) -> (Int, String)
func2 (prev, input) = if (length input) > 10
then func4 (prev + 1, take 9 input)
else (10, input)
-- Calls func2 on expanded version if a multiple of 3
func3 :: (Int, String) -> (Int, String)
func3 (prev, input) = if (length input) `mod` 3 == 0
then (prev + f2resI + 3, f2resStr)
else (prev + 1, tail input)
where
(f2resI, f2resStr) = func2 (prev, input ++ "ab")
func4 :: (Int, String) -> (Int, String)
func4 (prev, input) = if (length input) < 10
then (prev + length input, input ++ input)
else (prev + 5, take 5 input)
FirstДля ofначала, all,можно weотметить. canчто noticeструктура thatфункции thisнесколько functionтрудно structureобслуживаемая. isОпять, aмы littleпередаем bitдополнительные cumbersome.параметры. OnceВ again,частности, we'reмы passingотслеживаем aroundобщую extraстоимость, parameters.которая Inпоказывается particular,для we'reввода trackingи theвывода accumulatedкаждой cost,функции. whichМонада showsWriter
upдает asнам anпростой inputспособ andотслеживания anзначений. outputОна toтак eachже function.делает Theлегче Writerля monadнас providesотображение usстоиомсти withдля anразличных easierтипов. wayНо toчтобы trackпонять, thisкак value.мы Itдолжны wouldдля alsoначала makeизучить itдва easierтипокласса, forSemigroup
usи toMonoid
, representкоторые theпомогут costобощить with a different type. But to understand how, we should first learn two typeclasses, Semigroup and Monoid, that help us generalize accumulation.сбор.
SEMIGROUPS ANDи MONOIDS
ASemigroup
Semigroupэто isлюбой anyтип, typeкоторый thatмы weсобираем accumulate,с via anпомощью "append" operation.оператора. ThisЭта functionфункция usesиспользует theоператор operator <>
. ItОна combinesобъединяет twoдва elementsэлемента ofтипа theв typeновый, into a new, third element.третий.
class Semigroup a where
(<>) :: a -> a -> a
ForДля ourнашего firstпервого basicпростого example,примера, weмы canможем thinkдумать ofпредставить theInt
Intтип typeкак asчасть beingSemigroup
aпод Semigroupоперацией under the operation of addition:сложнения.
instance Semigroup Int where
a <> b = a + b
AMonoid
Monoidрасширяет extendsопределение theSemigroup
, definitionчтобы ofможно aбыло Semigroupвключить toопределяющий includeэлемент. anЭтот identityэлемент element.называется Thismempty
, elementтак isкак called mempty, since it is anэто "empty" elementэлементо ofсортировки. sorts.Отметим, Noticeчто howограничение aMonoid
constraintв ofтом, aчто Monoidон isуже thatдолжен itбыть should already be a Semigroup.Semigroup
.
class (Semigroup a) => Monoid a where
mempty :: a
ThisОпределяющий identityэлемент elementдолен shouldиметь haveсвойства, theесли propertyмы thatприбавшяем ifлюбой weдругой appendэлемент anya
, otherв elementлюбом направлении, результатом должен быть a
. toПоэтому it,результатом in either direction, the result should be a. That is, a <> mempty == a
andи mempty <> a == a
shouldвсегда alwaysдолжны beбыть true.true
. WeМы canможем extendрасширить ourнаше definitionопределение ofInt
theдля IntSemigroup
Semigroupдобавив by0
addingв "0"качестве asопределяющего theэлемента identityдля element of the Monoid.Monoid
.
instance Monoid Int where
memty = 0
WeМы canможем nowпродуктивно effectivelyиспользовать useInt
и собирать класс. Функция mempty
предлагает начальное значение для нешего моноида. Затем с помощью mappend
, мы объединяем два значения этого типа в результат. Это довольно легко, сделать экземпляр Monoid
для Int
. asНаш anсчетчик accumulationначинается class.с The0
, memptyи functionмы providesможем anобъединить initialзначения valueдля for our monoid. Then with mappend, we can combine two values of this type into a result. It is quite easy to how we can make a monoid instance for Int. Our accumulator starts at 0, and we combine values by adding them.добавления.
ThisЭтот Int
instanceэкземпляр isn'tне availableдоступен byпо defaultумолчания. though!Это Thisпотому, isчто becauseмы weможет couldтак equallyже wellпредоставить provideMonoid
aиз MonoidInt
fromиспользуя Intперемножение usingвместо multiplicationсложения. insteadВ ofэтом addition. In this case,случае, 1 becomesстановится the identity:определяющим.
instance Semigroup Int where
a <> b = a * b
instance Monoid Int where
mempty = 1
InВ bothобоих theseслучаях Int
examples,пример, ourнаша "append"append
functionфункция isсуммирующая. commutative.Базовая Inбиблиотека generalвключет though,экземпляр thisMonoid
doesn'tдля haveлюбого toтипа beList
. theОператор case.append
Theиспользует baseоператор librariesприбавления includeсписка an Monoid instance for any List type. The "append" operation uses the list append operator (++
, )whichкоторый isn'tне commutative!суммирующий. ThenВ theэтом identityслучае elementопределяющий isэлемент theэто emptyпустой list.список.
instance Semigroup [a] where
xs <> ys = xs ++ ys
instance Monoid [a] where
mempty = []
-- Not commutative!
-- [1, 2] <> [3, 4] == [1, 2, 3, 4]
-- [3, 4] <> [1, 2] == [3, 4, 1, 2]
USINGИспользование WRITER TOдля TRACK THEотслеживания ACCUMULATOR
SoКак howже doesэто thisпомогает helpнам usс withпроблемой ourсложения accumulation problem from before?выше?
TheМонада Writer
monadпараметризуется isс parameterizedпомощью byнекоторого someмоноидного monoidalтипа. type.Его Itsзадача jobследить isза toскладываемым keepзначением trackэтого ofтипа. anЕго accumulatedцель valueжить ofв thisконтексте type.глобальной Soпеременной itsкоторую operationsони liveмогут inменять. theПока contextReader
ofдает havingнам aвозмоность globalчитать valueглобальную thatпеременную, theyно canне modifyменять inеё thisWriter
particularпозволяет way.нам Soменять whileзначение Readerс hasпомощью aсложения, globalпри valueэтом weнельзя couldеё readчитать from,при butвычислении. notМы modify,можем вызвать операцию добавления используя tell
функцию в цели нашего выражения Writer
. allows us to modify a value by appending, through we can't directly read it during the compuation. We can call the appending operation by using the tell function in the course of our Writer expression:
tell :: a -> Writer a ()
JustТак asже withкак и с Reader
andи runReader,runReader
, thereесть isrunWriter
aфукнция. runWriterИ function.выглядит Itона looksнемного aпо little different:другому.
runWriter :: Writer w a -> (a, w)
WeНам don'tне needнужно toпредоставлять provideдополнительный anввод extraкроме inputвычислений besidesдля theзапуска. computationНо torunWriter
run. But runWriter producesосуществляет 2 outputs!вывода! TheПервый firstэто isрезультат theнашего finalвычисления. resultВторой of- ourпоследнее computation.сложенное Theзначение secondдля iswriter
. theМы finalне accumulatedпредоставили valueвходного ofзначения, theтак writer.как Weон provideавтоматически noиспользует initialmempty
accumulationиз value, because it will automatically use mempty from the Monoid!Monoid
!
Let'sДавайте exploreизучим howкак toизменить changeнаш ourкод codeвыше, fromчтобы aboveиспользовать toэту useмонаду. thisначнем monad.с We'll start with acc2:acc2
.
acc2' :: String -> Writer Int String
acc2' input = if (length input) > 10
then do
tell 1
acc4' (take 9 input)
else do
tell 10
return input
WeСоздаем branchотдельную onветку theпо lengthколичествую ofввходных theданных, input,и andдля thenкадой eachветки branchвыполняем isdo
. aБудем "do"использовать statement.tell
We'llдля useпредоставления tellсоответствующего toзначения provideдля theувеличивания appropriateсумматора, valueи toзатем incrementдвигается theк accumulator,вызову andследующей thenфункции, moveили onвозвращаем andответ. callзатем theacc3
nextи function, or return our answer. Then acc3 and acc4
. are similar.
acc3' :: String -> Writer Int String
acc3' input = if (length input) `mod` 3 == 0
then do
tell 3
acc2' (input ++ "ab")
else do
tell 1
return $ tail input
acc4' :: String -> Writer Int String
acc4' input = if (length input) < 10
then do
tell (length input)
return (input ++ input)
else do
tell 5
return (take 5 input)
Finally,Наконец, weмы don'tне changeменяем theтип typeподписи signatureнашей ofоригинальной ourфункци, originalвместо function,этого butмы weиспользуем insteadrunWriter
useдля runWriterвызова toпомощника, callкак ourи helpers, as appropriate.положено.
acc1' :: String -> (String, Int)
acc1' input = if length input `mod` 2 == 0
then runWriter (acc2' input)
else runWriter $ do
str1 <- acc3' (tail input)
str2 <- acc4' (take 1 input)
return (str1 ++ str2)
NoticeОтемтим, weнам noбольше longerне needнужно toявно actuallyотслеживать explicitly keep track of the accumulator. It is now wrapped by the Writer monad. We can increase it in any of our functions by calling "tell"сумматор. NowОн ourне codeобернут isс muchпомощью simplerwriter
andмонады. ourМы typesможем areувеличить cleaner.его в любой нашей функии вызвав tell
. Теперь наш код граздо проще а типы яснее.
CONCLUSIONВыводы
NowТеперь, thatзная weпро knowReader
aboutи theWriter
Readerмонады, andпришло Writerвремя monads,двигаться it'sдальше. timeДальше toмы moveобсудим onмонаду toState
. partЭта 5.монада There,объединяет we'llэти discussдве theидеи Stateв monad. This monad combines these two concepts into a read/write
, state,stateessentiallyестественно allowingпозволяя theиспользовать fullглобальные privilegeпеременные ofна aполную. globalЕсли variable.эти Ifидеи theseдо conceptsсих wereпор stillвас aсмущают, littleне confusing,бойтесь don'tперечитать be afraid to take another look at part 3 to solidify your understanding of monads.статью.
Maybe you've never programmed in Haskell before, and the realization that we can get all this cool functionality makes you want to try it! Download our Beginners Checklist to get started!