为什么要将Reader的构造函数参数定义为函数?

时间:2017-02-18 04:07:52

标签: haskell reader-monad

在学习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类型应该包含“environment”和“value”的信息,所以元组看起来很完美。

2 个答案:

答案 0 :(得分:5)

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

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生成... {/ p>

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定律的方式组合对中的第一个元素)。但是,这个Monad实例与Reader实例的做法非常不同。该对中的第一个元素未被修复(它可能会发生变化,因为我们mappend生成了其他值}并且我们不会使用它来计算该对的第二个元素(在上面的定义中,y既不依赖r也不依赖s)。 Writer是记录器;这里的r值是输出,而不是输入。

然而,有一种方法,你的直觉是合理的:我们不能使用一对像读者一样的monad,但我们可以做一个类似读者的 co <​​/ em> monad 。非常宽松地说,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基本相同{1}}一个(即您提供给Env的{​​{1}}):

(=>>)

作为进一步的证据,这是一个将一个变为另一个的函数:

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

答案 1 :(得分:3)

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

如果您使用元组描述Reader,则会出现问题:

  1. monad法律将被违反,例如左侧身份,其中指出:

    return a >>= f == f a
    
  2. 或正确的身份:

        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为常量值,不能通过另一个monadic操作更改,只读
    2. 但是当使用元组定义时,我们可以超级容易地覆盖读取器值,例如:这个功能:

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

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

      1. 此外,环境实际上不需要 之前将Reader转换为纯值(也就是运行Reader),所以仅此一点就可以了使用函数而不是元组的意义。
      2. 使用元组时,我们必须在实际执行操作之前提供Reader值。

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

        为了获得直觉,让我们假设Reader动作从Person返回age来自Addressbook的{​​{1}}:

          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个,所以它就像一个函数接受一个地址簿并返回一个{ {1}}列表, 所以很自然地将读者定义为从某个输入到结果的函数。

        我们可以将此[Person]操作重写为Reader的函数,如下所示:

        Addressbook

        但为什么要发明 personsWithThisAgeFun :: Int -> Addressbook -> [Person] personsWithThisAgeFun a addressbook = filter (\p -> age p == a) addressbook ??

        Reader的实际值显示何时组合多个函数,例如Reader,全部依赖于(相同)一个常量personsWithThisAge

        使用Addressbook我们不必明确传递一些Reader,个别Addressbook行动甚至根本无法修改Reader {1}} - Addressbook向我们保证,每个操作都使用相同的,未修改的 Reader,并且所有Addressbook操作都可以使用环境Reader

        实现这一目标的唯一方法是使用这些功能。

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

        现在你用元组描述的结构实际上非常接近Reader monad,什么是只写,但这超出了范围。