Зачем определять параметр конструктора Reader как функцию?
Изучая Монаду Читателя, я обнаружил, что она определяется как:
newtype Reader r a = Reader { runReader :: r -> a }
instance Monad (Reader r) where
return a = Reader $ \_ -> a
m >>= k = Reader $ \r -> runReader (k (runReader m r)) r
Я хочу знать, почему использовать функцию в качестве параметра конструктора, а не что-то еще, например, кортеж:
newtype Reader r a = Reader { runReader :: (r, a) }
instance Monad (Reader r) where
-- Here I cannot get r when defining return function,
-- so does that's the reason that must using a function whose input is an "r"?
return a = Reader (r_unknown, a)
m >>= k = Reader (fst $ runReader m) (f (snd $ runReader m))
Согласно определению Reader, нам нужна "среда", которую мы можем использовать для генерации "значения". Я думаю, что тип Reader должен содержать информацию "environment" и "value", поэтому кортеж кажется идеальным.
2 ответа
Вы не упомянули об этом в вопросе, но, думаю, вы подумали об использовании пары для определения Reader
потому что также имеет смысл думать об этом как о способе обеспечения фиксированной среды. Допустим, у нас есть более ранний результат в Reader
монада:
return 2 :: Reader Integer Integer
Мы можем использовать этот результат для дальнейших расчетов с фиксированной средой (и Monad
методы гарантируют, что он остается фиксированным по всей цепочке (>>=)
):
GHCi> runReader (return 2 >>= \x -> Reader (\r -> x + r)) 3
5
(Если вы подставите определения return
, (>>=)
а также runReader
в приведенном выше выражении и упростив его, вы увидите, как именно оно сводится к 2 + 3
.)
Теперь, давайте последуем вашему предложению и определим:
newtype Env r a = Env { runEnv :: (r, a) }
Если у нас есть среда типа r
и предыдущий результат типа a
мы можем сделать Env r a
из них...
Env (3, 2) :: Env Integer Integer
... и мы также можем получить новый результат от этого:
GHCi> (\(r, x) -> x + r) . runEnv $ Env (3, 2)
5
Вопрос в том, сможем ли мы запечатлеть эту модель через Monad
интерфейс. Ответ - нет. Пока есть Monad
Например, для пар, он делает что-то совсем другое:
newtype Writer r a = Writer { Writer :: (r, a) }
instance Monoid r => Monad (Writer r) where
return x = (mempty, x)
m >>= f = Writer
. (\(r, x) -> (\(s, y) -> (mappend r s, y)) $ f x)
$ runWriter m
Monoid
ограничение необходимо, чтобы мы могли использовать mempty
(который решает проблему, которую вы заметили, что нужно создать r_unknown
из ниоткуда) и mappend
(что позволяет комбинировать первые элементы пары таким образом, чтобы не нарушать законы монады). это Monad
экземпляр, однако, делает что-то совсем другое, чем то, что Reader
один делает. Первый элемент пары не зафиксирован (он может быть изменен, так как мы mappend
другие сгенерированные значения), и мы не используем его для вычисления второго элемента пары (в приведенном выше определении, y
не зависит ни от r
ни на s
). Writer
регистратор; r
значения здесь выводятся, а не вводятся.
Однако есть один способ, которым ваша интуиция оправдана: мы не можем создать читательскую монаду, используя пару, но мы можем создать читательскую комонаду. Чтобы сказать это очень свободно, Comonad
это то, что вы получаете, когда вы включаете Monad
интерфейс вверх ногами:
-- This is slightly different than what you'll find in Control.Comonad,
-- but it boils down to the same thing.
class Comonad w where
extract :: w a -> a -- compare with return
(=>>) :: w a -> (w a -> b) -> w b -- compare with (>>=)
Мы можем дать Env
мы отказались от Comonad
пример:
newtype Env r a = Env { runEnv :: (r, a) }
instance Comonad (Env r) where
extract (Env (_, x)) = x
w@(Env (r, _)) =>> f = Env (r, f w)
Это позволяет нам написать 2 + 3
пример с самого начала с точки зрения (=>>)
:
GHCi> runEnv $ Env (3, 2) =>> ((\(r, x) -> x + r) . runEnv)
(3,5)
Один из способов понять, почему это работает, это отметить, что a -> Reader r b
функция (то есть, что вы даете Reader
"s (>>=)
) по сути то же самое, что Env r a -> b
один (то есть, что вы даете Env
"s (=>>)
):
a -> Reader r b
a -> (r -> b) -- Unwrap the Reader result
r -> (a -> b) -- Flip the function
(r, a) -> b -- Uncurry the function
Env r a -> b -- Wrap the argument pair
Как еще одно доказательство этого, вот функция, которая превращает одну в другую:
GHCi> :t \f -> \w -> (\(r, x) -> runReader (f x) r) $ runEnv w
\f -> \w -> (\(r, x) -> runReader (f x) r) $ runEnv w
:: (t -> Reader r a) -> Env r t -> a
GHCi> -- Or, equivalently:
GHCi> :t \f -> uncurry (flip (runReader . f)) . runEnv
\f -> uncurry (flip (runReader . f)) . runEnv
:: (a -> Reader r c) -> Env r a -> c
Чтобы подвести итог, вот немного более длинный пример, с Reader
а также Env
версии бок о бок:
GHCi> :{
GHCi| flip runReader 3 $
GHCi| return 2 >>= \x ->
GHCi| Reader (\r -> x ^ r) >>= \y ->
GHCi| Reader (\r -> y - r)
GHCi| :}
5
GHCi> :{
GHCi| extract $
GHCi| Env (3, 2) =>> (\w ->
GHCi| (\(r, x) -> x ^ r) $ runEnv w) =>> (\z ->
GHCi| (\(r, x) -> x - r) $ runEnv z)
GHCi| :}
5
Прежде всего обратите внимание, что ваша функция привязки неверна и не будет компилироваться.
Если Reader
были определены как вы описываете с кортежем, будут проблемы:
Были бы нарушены законы монады, например, левая идентичность, которая гласит:
return a >>= f == f a
или правильная личность:
m >>= return == m
будет сломан, в зависимости от реализации >>=
так как >>=
забудет либо первый элемент кортежа первого аргумента, либо второго, т.е. если имплментация будет:
(Reader (mr, mv)) >>= f =
let (Reader (fr, fv)) = f mv
in Reader (mr, fv)
тогда мы всегда будем терять ценность читателя, которая исходит из f
(ака fr
) и в противном случае, если >>=
было бы
(Reader (mr, mv)) >>= f =
let (Reader (fr, fv)) = f mv
in Reader (fr, fv)
-- ^^^ tiny difference here ;)
мы бы всегда проиграли mr
,
Reader
какое-то действие, которое можетask
для постоянного значения, которое не может быть изменено другим монадическим действием, доступным только для чтения.
Но когда определено с помощью кортежа, мы можем очень просто перезаписать значение читателя, например, с помощью этой функции:
tell :: x -> BadReader x ()
tell x = BadReader (x, ())
Если читатель определен с помощью функции, это невозможно (попробуйте)
- Кроме того, эта среда на самом деле не требуется перед преобразованием
Reader
в чистое значение (то есть запуск Reader), так что от этого одного имеет смысл использовать функцию вместо кортежа.
При использовании кортежа мы должны предоставить Reader
значение даже до того, как мы действительно запустим действие.
Вы можете видеть это в своем return
определение, вы даже указываете на проблему, где r_unknown
происходит от...
Чтобы получить лучшую интуицию, давайте предположим, Reader
действие, которое возвращает Person
с определенным age
от Addressbook
:
data Person = MkPerson {name :: String, age :: Int}
type Addressbook = [Person]
personsWithThisAge :: Int -> Reader Addressbook [Person]
personsWithThisAge a = do
addressbook <- ask
return (filter (\p -> age p == a) addressbook)
это personsWithAge
функция возвращает Reader
действие и так как только ask
с для Addressbook
это как функция, которая принимает адресную книгу и возвращает [Person]
список, поэтому естественно определить читателя как функцию - функцию от некоторого ввода в результат.
Мы могли бы переписать это Reader
действие быть функцией Addressbook
как это:
personsWithThisAgeFun :: Int -> Addressbook -> [Person]
personsWithThisAgeFun a addressbook =
filter (\p -> age p == a) addressbook
Но зачем изобретать Reader
??
Реальная стоимость Reader
показывает при объединении нескольких функций, таких как, например, personsWithThisAge
, что все зависят от (одной и той же) одной константы Addressbook
,
Используя Reader
нам не нужно явно передавать некоторые Addressbook
вокруг, индивидуальный Reader
действия даже не имеют никакого способа изменить Addressbook
- Reader
гарантирует нам, что каждое действие использует то же самое, неизменное Addressbook
и все Reader
действие может когда-либо с окружающей средой ask
для этого.
Единственный способ реализовать это с этими гарантиями - с помощью функции.
Также, если вы посмотрите на экземпляры монады, которые включены в стандартную библиотеку, вы увидите, что (r ->)
это монада; на самом деле это идентично Reader
Монада кроме некоторых технических отличий.
Теперь структура, которую вы описываете с помощью кортежа, на самом деле довольно близка к Writer
монада, что только для записи, но это выходит за рамки.