访问函数中的环境

时间:2012-06-27 12:34:39

标签: haskell

main我可以阅读我的配置文件,并将其作为runReader (somefunc) myEnv提供。但somefunc不需要访问读者提供的myEnv,也不需要访问链中的下一对。需要来自myEnv的功能是一个很小的叶子功能。

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

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


我接受克里斯泰勒的,因为它是最彻底的,我可以看到它对其他人有用。还要感谢Heatsink,他是唯一一个试图直接回答我问题的人。

对于有问题的测试应用程序,我可能只是放弃了阅读器并传递环境。它不会给我任何东西。

我必须说我仍然感到困惑的是,为函数h提供静态数据不仅会改变其类型签名,还会改变调用它的g和调用g的f。即使所涉及的实际类型和计​​算没有改变,所有这些都是如此。看起来实施细节在整个代码中泄漏都没有真正的好处。

5 个答案:

答案 0 :(得分: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的相应类型为< / p>

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)

这可以起作用,事实上在语法方面它并不比使用Reader monad更糟糕。当您想要扩展语义时,就会遇到困难。假设您还依赖于具有类型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)

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

type Global = ReaderT Env (State Counter)

fg的旧定义继续有效。要将它们更新为具有应用程序计数语义,我们只需将它们更改为

即可
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接口需要更改三行 - 即使在我们更改了第一行之后程序仍继续工作,这意味着我们可以逐步进行重构(并在每次更改后进行测试),这会减少重构引入新错误的可能性。

答案 1 :(得分:5)

不。您完全将所有干预功能标记为Reader Env,或至少在具有Env环境的某个monad中运行。它完全到处传播。这是完全正常的 - 虽然没有您想象的那么低效,编译器通常会在很多地方优化这些东西。

基本上,任何使用Reader monad的东西 - 即使它非常远 - 都应该是Reader本身。 (如果某些内容不使用Reader monad,并且没有调用任何其他内容,则不一定是Reader。)

也就是说,使用Reader monad意味着你不必明确地传递周围的环境 - 它由monad自动处理。

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

答案 2 :(得分: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. Proper way to treat global flags in Haskell
  2. Global variables via unsafePerformIO in Haskell

答案 3 :(得分:3)

另一种可能有用的技术是传递叶函数本身,部分应用配置文件中的值。当然,只有能够以某种方式替换叶子功能才有利于此。

答案 4 :(得分:0)

如果您不希望将所有内容都放在Reader monad中的小叶函数中,那么您的数据是否允许您从Reader monad中提取必要的项目在顶级,然后将它们作为普通参数传递给叶函数?这样就不需要介于两者之间的所有东西都在Reader,但如果叶子函数确实需要知道它在Reader内部才能使用Reader的设施,那么你可以不必在Reader实例中运行它。