4

在学习 Reader Monad 的时候,我发现它是这样定义的:

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 类型应该包含“环境”和“价值”的信息,所以元组看起来很完美。

4

2 回答 2

6

您在问题中没有提到它,但我想您特别考虑过使用一对来定义Reader,因为将其视为提供固定环境的一种方式也是有意义的。假设我们在Readermonad 中有一个较早的结果:

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) }

如果我们有一个 type 的环境r和一个 type 的先前结果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函数(即你给's 的东西)与一个函数(即你给' Readers(>>=)的东西)本质上是一样的:Env r a -> bEnv(=>>)

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
于 2017-02-18T06:09:31.783 回答
3

首先请注意,您的绑定函数是错误的并且不会编译。

如果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(aka fr) 的读者价值,否则如果>>=

(Reader (mr, mv)) >>= f =
    let (Reader (fr, fv)) = f mv 
    in Reader (fr, fv) 
           -- ^^^ tiny difference here ;)

我们总是会输mr

  1. AReader是一些动作,可能ask是一个常数值,它不能被另一个单子动作改变,它是只读的

但是当使用元组定义时,我们可以非常容易地覆盖读取器值,例如使用这个函数:

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

如果阅读器是用函数定义的,这是不可能的(试试看)

  1. 此外,在将 a 转换为纯值(也就是运行 Reader)之前,实际上不需要该环境,因此仅从这一点来看,使用函数而不是元组是有意义的。Reader

使用元组时,我们Reader甚至必须在实际运行操作之前提供值。

您可以看到,在您的return定义中,您甚至指出了问题的r_unknown来源......

为了获得更好的直觉,让我们假设一个Reader动作从 s 中返回具有Person特定值的 s :ageAddressbook

  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用于 s Addressbook,它就像一个接受地址簿并返回一个[Person]列表的函数,所以很自然地将 reader 定义为从某个输入到结果的函数。

Reader我们可以将这个动作重写为这样的函数Addressbook

  personsWithThisAgeFun :: Int -> Addressbook -> [Person]
  personsWithThisAgeFun a addressbook =
    filter (\p -> age p == a) addressbook

但为什么要发明Reader??

当组合几个函数时显示的真正价值,Reader例如personsWithThisAge,都依赖于(相同的)一个常数Addressbook

使用 aReader我们不必明确地传递一些Addressbook东西,单个Reader动作甚至根本没有任何方式来修改Addressbook-向Reader我们保证,每个动作都使用相同的、未修改的 Addressbook,并且所有Reader动作都可以与环境一起使用是ask为了它。

具有这些保证的唯一方法是使用函数。

此外,如果您查看标准库中包含的 monad 实例,您会发现这(r ->)是一个 monad;Reader实际上,除了一些技术差异之外,它与monad 相同。

现在,您用元组描述的结构实际上非常接近Writer单子,即write-only,但这超出了范围。

于 2017-02-18T05:27:30.787 回答