在main
我可以阅读我的配置文件,并将其作为runReader (somefunc) myEnv
提供。但somefunc
不需要访问读者提供的myEnv
,也不需要访问链中的下一对。需要来自myEnv的功能是一个很小的叶子功能。
如何在不将所有干预功能标记为(Reader Env)
的情况下访问函数中的环境?这可能不对,因为否则你只是首先传递myEnv。通过多个级别的函数传递未使用的参数只是丑陋(不是吗?)。
我可以在网上找到很多例子,但它们似乎在runReader和访问环境之间只有一个级别。
我接受克里斯泰勒的,因为它是最彻底的,我可以看到它对其他人有用。还要感谢Heatsink,他是唯一一个试图直接回答我问题的人。
对于有问题的测试应用程序,我可能只是放弃了阅读器并传递环境。它不会给我任何东西。
我必须说我仍然感到困惑的是,为函数h提供静态数据不仅会改变其类型签名,还会改变调用它的g和调用g的f。即使所涉及的实际类型和计算没有改变,所有这些都是如此。看起来实施细节在整个代码中泄漏都没有真正的好处。
答案 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)
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接口需要更改三行 - 即使在我们更改了第一行之后程序仍继续工作,这意味着我们可以逐步进行重构(并在每次更改后进行测试),这会减少重构引入新错误的可能性。
答案 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
答案 3 :(得分:3)
另一种可能有用的技术是传递叶函数本身,部分应用配置文件中的值。当然,只有能够以某种方式替换叶子功能才有利于此。
答案 4 :(得分:0)
如果您不希望将所有内容都放在Reader
monad中的小叶函数中,那么您的数据是否允许您从Reader
monad中提取必要的项目在顶级,然后将它们作为普通参数传递给叶函数?这样就不需要介于两者之间的所有东西都在Reader
,但如果叶子函数确实需要知道它在Reader
内部才能使用Reader
的设施,那么你可以不必在Reader
实例中运行它。