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

  1. Были бы нарушены законы монады, например, левая идентичность, которая гласит:

    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,

  1. Reader какое-то действие, которое может ask для постоянного значения, которое не может быть изменено другим монадическим действием, доступным только для чтения.

Но когда определено с помощью кортежа, мы можем очень просто перезаписать значение читателя, например, с помощью этой функции:

    tell :: x -> BadReader x ()
    tell x = BadReader (x, ())

Если читатель определен с помощью функции, это невозможно (попробуйте)

  1. Кроме того, эта среда на самом деле не требуется перед преобразованием 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 монада, что только для записи, но это выходит за рамки.

Другие вопросы по тегам