Зачем определять параметр конструктора 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 должен содержать информацию о «среде» и «значении», поэтому кортеж кажется идеальным.


person hliu    schedule 18.02.2017    source источник
comment
Просто чтобы прояснить ваши сомнения: почему вы подумали именно о кортеже?   -  person duplode    schedule 18.02.2017
comment
Похоже, вы сами отвечаете на свой вопрос...?   -  person Justin L.    schedule 18.02.2017
comment
@ДжастинЛ. Я подозреваю, что есть более интересный основной вопрос, который явно не указан в OP - имеет ли смысл использовать пару для тех же целей, что и Reader.   -  person duplode    schedule 18.02.2017
comment
@duplode Потому что, согласно определению Reader, нам нужна среда, которую мы можем использовать для генерации значения. Я думаю, что тип Reader должен содержать информацию о среде и значении, поэтому кортеж кажется идеальным.   -  person hliu    schedule 18.02.2017
comment
@hliu Вот что я подозревал :) Я пишу ответ по этому поводу.   -  person duplode    schedule 18.02.2017
comment
@duplode Да, я пытался и хочу ограничить r классом Monoid (чтобы я мог указать ему значение при определении возврата), но это действительно похоже на монаду Writer. Возможно, смысл Reader в том, чтобы использовать среду как input для генерации значения, но Writer может просто сгенерировать саму среду, используя функцию tell. Извините за мой плохой английский..   -  person hliu    schedule 18.02.2017


Ответы (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 (что позволяет комбинировать первые элементы пары таким образом, t нарушают законы монад). Однако этот экземпляр Monad делает что-то совершенно отличное от того, что делает Reader. Первый элемент пары не является фиксированным (он может быть изменен, поскольку мы mappend присваиваем ему другие сгенерированные значения), и мы не используем его для вычисления второго элемента пары (в приведенном выше определении y не используется). не зависят ни от r, ни от s). Writer — регистратор; значения r здесь выводятся, а не вводятся.


Однако есть один способ, в котором ваша интуиция оправдывается: мы не можем создать монаду, подобную читателю, используя пару, но мы можем создать монаду, подобную читателю, co. Проще говоря, 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) — это, по сути, то же самое, что и функция Env r a -> b (то есть то, что вы даете (=>>) из Env):

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
person duplode    schedule 18.02.2017

Прежде всего обратите внимание, что ваша функция связывания неверна и не будет компилироваться.

Если бы 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 еще до того, как мы действительно запустим действие.

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

person sheyll    schedule 18.02.2017