1

我有一个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
  ...

这里值得注意的是:

  • 我们有一个mainMonad既是 aMonadReader Environment又是 a 的主 monad ( ) MonadRandom
  • 主 monad 调用相同类型的从属monadAmonad monadB
  • 我们有第四个单子作为 和 的monadA助手monadB
  • monadB呼吁monadA(但用于local改变环境)

最重要的是:

  • 无论何时monadA或被helperA称为EnvironmentDataEnvironmentA,无论何时monadB被称为EnvironmentDataEnvironmentB

我的代码库几乎是这个的放大版本。有更多从属的 Monad(目前有 12 个,但将来可能会增加),有更多的助手,而且我的EnvironmentData类型有点复杂(我的Environment虽然几乎相同)。

最后一个要点很重要,因为EnvironmentData在帮助程序中使用了 并且错误Environment会导致帮助程序的结果发生细微的变化。

现在我的问题是很容易local在我的代码中遗漏 a 并且直接在错误的环境中调用 monad。我也害怕在不使用的情况下调用 monad,local因为我认为它期待一个它不是的环境。这些错误很小且很容易出错(我已经做过好几次了),而且这样做的结果通常是相当微妙和变化多端的。这最终使问题的症状很难通过单元测试来捕捉。所以我想直接针对问题。我的第一个直觉是在我的单元测试中添加一个子句,其中包含以下内容:

调用mainMonad检查在评估它的过程中,我们从来没有在错误的环境中调用过 monad。

这样我就可以发现这些错误,而不必非常仔细地梳理代码。现在在考虑了一会儿之后,我还没有想出一个非常简洁的方法来做到这一点。我想到了几种可行的方法,但我不太满意:

1. 错误环境调用硬崩溃

我可以通过在每个 monad 的前面添加一个条件来解决这个问题,如果它检测到它被错误的环境调用,就会硬崩溃。例如:

monadA ::
     ( MonadReader m
     )
       => m Type
    monadA = do
      env <- view _1  ask
      case env of
        EnvironmentA -> return ()
        _ -> undefined
      ...

崩溃将在单元测试期间被捕获,我会发现问题。然而,这并不理想,因为我真的希望客户体验由于使用错误环境调用事物而导致的轻微问题,而不是在测试处理程序没有发现问题的情况下发生硬崩溃。这有点像核选项。这并不糟糕,但以我的标准和三者中最差的标准来说并不令人满意。

2.使用类型安全

我还尝试更改的类型,monadA因此monadB不能monadA直接从中调用,monadB反之亦然。这非常好,因为它可以在编译时捕获问题。这存在维护起来有点痛苦的问题,而且非常复杂。因为monadA并且monadB可能每个都共享许多类型的公共单子,所以(MonadReader m) => m Type每个单子也必须被提升。真的,它几乎可以保证现在每条线路都有电梯。我不反对基于类型的解决方案,但我不想花费大量时间来维护单元测试。

3. 将局部变量移到声明的内部

每个对 有限制的 monadEnvironmentData都可以从类似于以下的样板开始:

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

然后使用(如下面的Daniel Wagner所建议的)添加一个包装器到来自我的 s. 由于没有环境数据,我不能错误地称呼它们。这几乎与最后一个问题完全相同。runReaderT withReaderTMonadReader EnvironmentEnvironmentData


那么有没有一种方法可以确保我的 monad 总是从正确的环境中调用?

4

3 回答 3

1

虽然看起来有点奇怪,但我想一种方法是引入冗余ReaderT

 data EnvironmentA -- = ...
 data EnvironmentB -- = ...

 convertAToB :: EnvironmentA -> EnvironmentB
 convertBToA :: EnvironmentB -> EnvironmentA
 -- convertAToB = ...
 -- convertBToA = ...

 monadA :: MonadReader EnvironmentA m => m Type
 monadA = do
     env <- ask
     -- ...
     res <- runReaderT monadB (convertAToB env)
     -- ...

 monadB :: MonadReader EnvironmentB m => m Type
 monadB = do
     env <- ask
     -- ...
     res <- runReaderT monadA (convertBToA env)
     -- ...
于 2019-07-22T22:16:36.570 回答
1

您的示例有点过于简化,我无法判断它的适用程度,但您也可以通过将 Environment 类型参数化来获得。也许是 GADT,例如:

data Environment t where
    EnvironmentA :: Environment A
    EnvironmentB :: Environment B

data A
data B

然后,关心它在哪个特定环境中运行的代码可以有一个MonadReader (Environment A) mMonadReader (Environment B) m约束,而同时使用两者的代码可以使用一个MonadReader (Environment t) m约束。

这种方法的唯一缺点是标准 GADT 有时需要小心分支以确保编译器有适当的类型相等证明。它通常可以完成,但需要多加注意。

于 2019-07-23T01:41:18.087 回答
0

这是我将采取的方法。根据@Carl 的回答,我将通过使用由类型“标签”参数化的 GADT 在类型级别区分“A”和“B”环境。为标签使用一对空类型(data Aand data B,就像@Carl 所做的那样)有效,但我更喜欢使用DataKinds它,因为它使意图更清晰。

以下是预赛:

{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

import Control.Monad.Reader
import Control.Monad.Random

这是环境类型的定义:

data EnvType = A | B
data Environment (e :: EnvType) where
  EnvironmentA :: Integer -> Environment 'A
  EnvironmentB :: Integer -> Environment 'B

在这里,不同的环境碰巧具有相同的内部结构(即,它们每个都包含一个Integer),但并不要求它们必须这样做。

我将做一个简化的假设,即您的 monad 始终将环境ReaderT作为最外层,但我们将在基本 monad 中保持多态性(因此您可以使用IOGen提供随机性)。您可以改用约束来完成所有这些操作MonadReader,但是由于一些晦涩的技术原因,事情变得更加复杂(如果您真的需要这个,请添加评论,我会尝试发布补充答案)。也就是说,对于任意基础 monad b,我们将在 monad 中工作:

type E e b = ReaderT (Environment e) b

现在,我们可以mainMonad如下定义动作。请注意没有MonadReader约束,因为这是由E e b Type签名处理的。对基本 monad的MonadRandom b约束确保E e b它将有一个MonadRandom实例。因为签名E e b Type在 中是多态的e :: EnvType,所以mainMonad可以在任何类型的环境中工作。通过环境 GADT 上的大小写匹配,它可以将约束e ~ 'A等带入范围,允许它分派到monadA等。

data Type = Type [String]  -- some return type

mainMonad ::
  ( MonadRandom b )
    => E e b Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentA _ -> monadA
    EnvironmentB _ -> monadB

monadA和的类型签名monadB相似,尽管它们修复了EnvType

monadA ::
  ( MonadRandom b )
    => E 'A b Type
monadB ::
  ( MonadRandom b )
    => E 'B b Type

monadA动作可以调用 A-specifichelperA以及 common helper

monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type [result1,  result2]

助手可以使用这些MonadRandom设施并使用环境getData中的案例匹配等功能检查环境。

helperA :: (Monad b) => E 'A b String -- we don't need MonadRandom on this one
helperA = do
  n <- asks getData
  return $ show n
helper :: (MonadRandom b) => E e b String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x
getData :: Environment e -> Integer
getData (EnvironmentA x) = x
getData (EnvironmentB x) = x

也可以直接在环境中进行大小写匹配。在通用助手中,所有环境类型都需要处理,但在EnvType特定助手中,只EnvType需要处理(即,模式匹配将是详尽的,因此即使使用-Wall,也不会生成有关不匹配情况的警告):

helper2 :: (Monad b) => E e b String
helper2 = do
  env <- ask
  case env of
    -- all cases must be handled or you get "non-exhaustive" warnings
    EnvironmentA n -> return $ show n ++ " with 'A'-appropriate processing"
    EnvironmentB n -> return $ show n ++ " with 'B'-appropriate processing"
helperA2 :: (Monad b) => E 'A b String
helperA2 = do
  env <- ask
  case env of
    -- only A-case need be handled, and trying to match B-case generates warning
    EnvironmentA n -> return $ show n

monadB操作可以调用通用助手,并可以monadA通过适当的withReaderT调用进行调度。

monadB = do
  Type start <- withReaderT envBtoA monadA
  result <- helper
  return $ Type $ start ++ [result]

envBtoA :: Environment 'B -> Environment 'A
envBtoA (EnvironmentB x) = EnvironmentA x

当然,最重要的是,您不能意外地从 B 类操作调用 A 类操作:

badMonadB ::
  ( MonadRandom b )
    => E 'B b Type
badMonadB = do
  monadA  -- error: couldn't match A with B

您也不能不小心从通用助手调用 A 类型操作:

-- this is a common helper
badHelper :: (Monad b) => E e b String
badHelper = do
  -- so it can't assume EnvironmentA is available
  helperA  -- error: couldn't match "e" with B

尽管您可以使用大小写匹配来检查适当的环境,然后分派:

goodHelper :: (Monad b) => E e b String
goodHelper = do
  env <- ask
  case env of
    EnvironmentA _ -> helperA  -- if we're "A", it's okay
    _              -> return "default"

我觉得我应该指出 @DanielWagner 的解决方案的相对优缺点(我认为你误解了)。

他的解决方案:

  • 确实提供类型安全。如果您尝试坚持res <- monadB的定义monadA,它将不会进行类型检查。
  • 正如所写的那样,没有提供一种机制来定义访问环境的通用帮助函数(只需要的通用帮助函数就MonadRandom可以正常工作),但这可以通过引入一个带有实例的类型类来完成,EnvironmentAEnvironmentB提供方法来做任何事情允许普通助手处理环境
  • 需要对环境进行特殊处理mainMonad(尽管有一些关于为什么首先需要的问题mainMonad
  • 避免高级类型级别的技巧,因此可能更容易使用
  • 我相信每个环境转换都会增加一个额外的ReaderT层,因此如果存在深度递归 A-to-B-to-A-to-B 嵌套,可能会导致运行时损失。

要并排查看它们,这是我的完整解决方案:

{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE DataKinds, GADTs, KindSignatures #-}

import Control.Monad.Reader
import Control.Monad.Random

data EnvType = A | B
data Environment (e :: EnvType) where
  EnvironmentA :: Integer -> Environment 'A
  EnvironmentB :: Integer -> Environment 'B
getData :: Environment e -> Integer
getData (EnvironmentA x) = x
getData (EnvironmentB x) = x

type E e b = ReaderT (Environment e) b

data Type = Type [String]  -- some return type

mainMonad :: (MonadRandom b) => E e b Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentA _ -> monadA
    EnvironmentB _ -> monadB

monadA :: (MonadRandom b) => E 'A b Type
monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type [result1,  result2]

monadB :: (MonadRandom b) => E 'B b Type
monadB = do
  Type start <- withReaderT envBtoA monadA
  result <- helper
  return $ Type $ start ++ [result]
envBtoA :: Environment 'B -> Environment 'A
envBtoA (EnvironmentB x) = EnvironmentA x

helperA :: (Monad b) => E 'A b String -- we don't need MonadRandom on this one
helperA = do
  n <- asks getData
  return $ show n

helper :: (MonadRandom b) => E e b String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x

这是他的一个版本:

{-# OPTIONS_GHC -Wall -Wincomplete-uni-patterns #-}
{-# LANGUAGE FlexibleContexts #-}

import Control.Monad.Reader
import Control.Monad.Random

data EnvType = A | B
data EnvironmentMain = EnvironmentMain EnvType Integer
data EnvironmentA = EnvironmentA Integer
data EnvironmentB = EnvironmentB Integer

class Environment e where getData :: e -> Integer
instance Environment EnvironmentA where getData (EnvironmentA n) = n
instance Environment EnvironmentB where getData (EnvironmentB n) = n

convertAToB :: EnvironmentA -> EnvironmentB
convertAToB (EnvironmentA x) = EnvironmentB x
convertBToA :: EnvironmentB -> EnvironmentA
convertBToA (EnvironmentB x) = EnvironmentA x

data Type = Type [String]  -- some return type

mainMonad :: (MonadReader EnvironmentMain m, MonadRandom m) => m Type
mainMonad = do
  env <- ask
  case env of
    EnvironmentMain A n -> runReaderT monadA (EnvironmentA n)
    EnvironmentMain B n -> runReaderT monadB (EnvironmentB n)

monadA :: (MonadReader EnvironmentA m, MonadRandom m) => m Type
monadA = do
  result1 <- helperA
  result2 <- helper
  return $ Type $ [result1] ++ [result2]

monadB :: (MonadReader EnvironmentB m, MonadRandom m) => m Type
monadB = do
  env <- ask
  Type start <- runReaderT monadA (convertBToA env)
  result <- helper
  return $ Type $ start ++ [result]

helperA :: (MonadReader EnvironmentA m) => m String
helperA = do
  EnvironmentA n <- ask
  return $ show n

helper :: (Environment e, MonadReader e m, MonadRandom m) => m String
helper = do
  n <- asks getData
  x <- getRandomR (0,n)
  return $ show x
于 2019-07-25T20:54:17.147 回答