11

main我可以读取我的配置文件,并提供它runReader (somefunc) myEnv就好了。但somefunc不需要访问myEnv阅读器用品,链中的下一对也不需要。需要来自 myEnv 的函数是一个小叶函数。

如何在不将所有干预函数标记为的情况下访问函数中的环境(Reader Env)?那不可能是正确的,因为否则您首先会传递 myEnv 。并且通过多个级别的函数传递未使用的参数只是丑陋的(不是吗?)。

我可以在网上找到很多示例,但它们似乎都在 runReader 和访问环境之间只有一个级别。


我接受 Chris Taylor 的,因为它是最彻底的,而且我认为它对其他人有用。也感谢 Heatsink,他是唯一一个试图直接回答我问题的人。

对于有问题的测试应用程序,我可能会完全放弃 Reader 并传递环境。它没有给我任何东西。

我必须说我仍然对这样的想法感到困惑,即为函数 h 提供静态数据不仅会改变它的类型签名,还会改变调用它的 g 和调用 g 的 f 的类型签名。即使所涉及的实际类型和计​​算没有改变,这一切也是如此。似乎实现细节在代码中泄漏,没有真正的好处。

4

5 回答 5

8

确实为所有内容提供了返回类型Reader Env a,尽管这并不像您想象的那么糟糕。一切都需要这个标签的原因是,如果f取决于环境:

type Env = Int

f :: Int -> Reader Int Int
f x = do
  env <- ask
  return (x + env)

g调用f

g x = do
  y <- f x
  return (x + y)

然后g还取决于环境 - 行中绑定的值y <- f x可能不同,具体取决于传入的环境,因此适当的类型g

g :: Int -> Reader Int Int

这其实是一件好事!类型系统迫使您明确识别函数依赖于全局环境的位置。您可以通过为该短语定义一个快捷方式来节省一些打字痛苦Reader Int

type Global = Reader Int

所以现在你的类型注释是:

f, g :: Int -> Global Int

这更具可读性。


对此的替代方法是将环境显式传递给您的所有函数:

f :: Env -> Int -> Int
f env x = x + env

g :: Env -> Int -> Int
g x = x + (f env x)

这可以工作,事实上在语法方面它并不比使用Readermonad 差。当您想要扩展语义时,困难就来了。假设您还依赖于Int计算功能应用程序的类型的可更新状态。现在您必须将功能更改为:

type Counter = Int

f :: Env -> Counter -> Int -> (Int, Counter)
f env counter x = (x + env, counter + 1)

g :: Env -> Counter -> Int -> (Int, Counter)
g env counter x = let (y, newcounter) = f env counter x
                  in (x + y, newcounter + 1)

这显然不那么令人愉快。另一方面,如果我们采用单子方法,我们只需重新定义

type Global = ReaderT Env (State Counter)

f和的旧定义g继续有效,没有任何麻烦。为了更新它们以具有应用程序计数语义,我们只需将它们更改为

f :: Int -> Global Int
f x = do
  modify (+1)
  env <- ask
  return (x + env)

g :: Int -> Global Int
g x = do
  modify(+1)
  y <- f x
  return (x + y)

他们现在完美地工作。比较两种方法:

  • 当我们想向程序添加新功能时,显式传递环境和状态需要完全重写。

  • 使用 monadic 接口需要更改三行 - 即使在我们更改了第一行之后程序仍然可以工作,这意味着我们可以增量地进行重构(并在每次更改后对其进行测试),从而降低了重构引入的可能性新的错误。

于 2012-06-27T14:45:49.863 回答
5

没有。您完全可以将所有中间函数标记为Reader Env,或者至少标记为在某个带有Env环境的 monad 中运行。它确实到处传播。这是完全正常的——尽管没有你想象的那么低效,而且编译器经常会在很多地方优化这些东西。

基本上,任何使用Readermonad 的东西——即使它很远——都应该是 aReader本身。(如果某些东西不使用Readermonad,并且不调用其他任何东西,它不必是Reader.)

也就是说,使用Readermonad 意味着您不必显式地传递环境——它由 monad 自动处理。

(请记住,它只是指向环境的指针,而不是环境本身,所以它非常便宜。)

于 2012-06-27T13:19:16.813 回答
3

这些是真正的全局变量,因为它们在main. 对于这种情况,使用全局变量是合适的。如果需要 IO,您必须使用unsafePerformIO它们来编写它们。

如果您只是读取配置文件,则非常简单:

config :: Config
{-# NOINLINE config #-}
config = unsafePerformIO readConfigurationFile

如果对其他代码有一些依赖,以至于你必须控制何时加载配置文件,那就更复杂了:

globalConfig :: MVar Config
{-# NOINLINE globalConfig #-}
globalConfig = unsafePerformIO newEmptyMVar

-- Call this from 'main'
initializeGlobalConfig :: Config -> IO ()
initializeGlobalConfig x = putMVar globalConfig x

config :: Config
config = unsafePerformIO $ do
  when (isEmptyMVar globalConfig) $ fail "Configuration has not been loaded"
  readMVar globalConfig

也可以看看:

  1. 在 Haskell 中处理全局标志的正确方法
  2. Haskell中通过unsafePerformIO的全局变量
于 2012-06-27T14:02:43.503 回答
3

另一种可能有用的技术是传递叶子函数本身,部分应用来自配置文件的值。当然,这只有在能够以某种方式替换叶子函数对您有利的情况下才有意义。

于 2012-06-27T14:17:28.760 回答
0

如果您不想将所有小叶函数都放在monad 中,您的数据是否允许您从顶层Reader的 monad 中提取必要的项目,然后将它们作为普通参数向下传递Reader通过叶子函数?这将消除介于两者之间的所有内容的需要 in Reader,尽管如果叶函数确实需要知道它在内部Reader以便使用Reader's 设施,那么您就不得不在您的Reader实例中运行它。

于 2012-06-27T13:54:08.120 回答