【发布时间】:2019-11-30 21:45:13
【问题描述】:
我有一个MonadReader,它为我正在处理的应用程序生成数据。这里的主要 monad 根据一些环境变量生成数据。 monad 通过根据环境选择要运行的其他几个 monad 之一来生成数据。我的代码看起来有点像下面的 mainMonad 是主要的单子:
data EnvironmentData = EnvironmentA | EnvironmentB
type Environment = (EnvironmentData, Integer)
mainMonad ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
mainMonad = do
env <- ask
case env of
EnvironmentA -> monadA
EnvironmentB -> monadB
monadA ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadA = do
...
result <- helperA
result <- helper
...
monadB ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadB = do
start <- local (set _1 EnvironmentA) monadA
...
result <- helper
...
helperA ::
( MonadReader Environment m
, MonadRandom m
)
=> m String
helperA = do
...
helper ::
( MonadReader Environment m
, MonadRandom m
)
=> m String
helper = do
...
这里值得注意的是:
- 我们有一个主 monad (
mainMonad),它既是MonadReader Environment,又是MonadRandom。 - 主 monad 调用同一类型的从属 monad
monadA和monadB。 - 我们有第四个 monad 作为
monadA和monadB的助手。 -
monadB调用monadA(但使用local更改环境)
最重要的是:
- 每当
monadA或helperA被调用时,EnvironmentData就是EnvironmentA,而每当monadB被调用时,EnvironmentData就是EnvironmentB。
我的代码库几乎是这个的放大版本。有更多的从属 Monad(目前 12 个,但未来可能会增加),有更多的助手,而且我的 EnvironmentData 类型更复杂一些(虽然我的 Environment 几乎相同)。
最后一个要点很重要,因为EnvironmentData 用于助手中,错误的Environment 会导致助手结果的细微变化。
现在我的问题是,在我的代码中很容易错过 local,而直接在错误的环境中调用 monad。我也担心在不使用local 的情况下调用 monad,因为我认为它期望的环境并非如此。这些错误很小且容易出错(我已经做过好几次了),而且这样做的结果通常是相当微妙和变化多端的。这最终使问题的症状很难通过单元测试来捕捉。所以我想直接针对问题。我的第一个直觉是在我的单元测试中添加一个子句,其中包含以下内容:
调用
mainMonad检查在评估它的过程中,我们从来没有在错误的环境中调用过 monad。
这样我就可以发现这些错误,而不必非常仔细地梳理代码。现在在考虑了一会儿之后,我还没有想出一个非常简洁的方法来做到这一点。我已经想到了几种可行的方法,但我不太满意:
1。使用错误的环境调用时发生严重崩溃
我可以通过在每个 monad 的前面添加一个条件来解决这个问题,如果它检测到它被错误的环境调用,就会硬崩溃。例如:
monadA ::
( MonadReader m
)
=> m Type
monadA = do
env <- view _1 ask
case env of
EnvironmentA -> return ()
_ -> undefined
...
在单元测试期间会发现崩溃,我会发现问题。然而,这并不理想,因为我真的希望客户体验由于使用错误环境调用事物而导致的轻微问题,而不是在测试处理程序没有发现问题的情况下发生硬崩溃。这有点像核选项。这并不糟糕,但以我的标准和三者中最差的标准来说并不令人满意。
2。使用类型安全
我还尝试更改monadA 和monadB 的类型,以便不能直接从monadB 调用monadA,反之亦然。这非常好,因为它可以在编译时捕获问题。这存在维护起来有点痛苦的问题,而且非常复杂。由于monadA 和monadB 可能各自共享许多(MonadReader m) => m Type 类型的公共单子,因此每个单子都必须被提升。真的,它几乎可以保证现在每条线路都有电梯。我不反对基于类型的解决方案,但我不想花费大量时间来维护单元测试。
3。在声明中移动本地人
每个对 EnvironmentData 有限制的 monad 都可以从类似于以下的样板开始:
monadA ::
( MonadReader Environment m
, MonadRandom m
)
=> m Type
monadA = do
env <- view _1 <$> ask
case env of
EnvironmentA ->
...
_ ->
local (set _1 EnvironmentA) monadA
这很好,因为它确保始终在正确的环境中调用所有内容。然而,问题在于它以一种单元测试或类型证明没有的方式默默地“修复”错误。它真的只是防止我忘记local。
3.5。删除EnvironmentData
这个基本上等同于上一个,不过可能更干净一些。如果我将monadA 和monadB 的类型更改为
( MonadReader Integer m
, MonadRandom m
)
=> m Type
然后使用 runReaderTwithReaderT(如下面的Daniel Wagner 建议)为来自我的MonadReader Environments 的呼叫添加一个包装器。我不能用错误的EnvironmentData 给他们打电话,因为没有环境数据。这几乎与上一个问题完全相同。
那么有没有一种方法可以确保我的 monad 总是从正确的环境中调用?
【问题讨论】:
-
你也可以看看
withReaderT,尽管这需要你选择一个特定的monad堆栈(或者至少它的外部位直到ReaderT)而不是使用mtl风格类多态性。 -
根据您的描述,基于类型的解决方案显然是正确的方法,不应该要求您取消所有操作(例如,@DanielWagner 的回答)。如果您发现他的回答不令人满意,我怀疑我们可以提供更好的东西,但是您的最小示例太抽象且太伪代码(例如,这些类型签名对于
MonadReader的通常定义无效)。您能否发布一个更真实的玩具示例来编译并说明您正在尝试做的事情? -
@K.A.Buhr 感谢您的反馈。我让事情变得更加具体(我还修复了我的类型签名,这只是我的一个错误)。如果有人愿意,我可以让它更真实,甚至进一步填写特定部分。我只是不确定与这种东西分享多少。
-
在这个应用程序中使用 monad 约束(例如,
MonadReader)而不是具体的 monad 类型对您有多重要?我知道有些人提倡将此作为最佳实践,但您是否真的需要在多个具体 monad 上以多态方式运行此代码? -
@K.A.Buhr 不幸的是,我使用
MonadRandom而不是特定环境实际上相当重要。这是因为测试处理程序使用Gen作为实例,而应用程序本身使用IO。
标签: haskell testing hspec reader-monad