编写简单的 QuickCheck URL 生成器时嵌套单子的问题

Problem with nested monads while writing a simple QuickCheck URL generator

另一个新手问题,可能是因为我没有掌握 Haskell 中的 Monadic do:我想使用 Text.URI 为格式良好的 URI 编写一个简单的 QuickCheck 生成器从 modern-uri 包中输入。据我了解,这里涉及两种类型的 monad:MonadThrow 用于 URI 构造时的错误处理,以及来自 QuickCheck 的 Gen

这是我尝试实现生成器的尝试。它不进行类型检查:

import qualified Text.URI as URI

uriGen :: Gen URI.URI
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen -- (:: Gen Text), a simple generator for printable text. 
    uri <- do
        scheme <- URI.mkScheme sc
        host <- URI.mkHost $ (hostName <> "." <> tld)
        return $ URI.URI (Just scheme) (Right (URI.Authority Nothing host Nothing)) Nothing [] Nothing
    return uri

我的理解是,外部 do 块属于 Gen monad,而内部块处理 MonadThrow。我尝试从 Gen 中解开 Text 部分,然后使用解开的 Text 构建 URI 部分,从 MonadThrow 中解开它们,然后重新组装整个 URI,最后将其包装在一个新的 Gen.

但是,我收到以下类型检查错误:

    • No instance for (MonadThrow Gen)
        arising from a use of ‘URI.mkScheme’
    • In a stmt of a 'do' block: scheme <- URI.mkScheme sc
      In a stmt of a 'do' block:
        uri <- do scheme <- URI.mkScheme sc
                  host <- URI.mkHost $ (hostName <> "." <> tld)
                  return
                    $ URI.URI
                        (Just scheme)
                        (Right (URI.Authority Nothing host Nothing))
                        Nothing
                        []
                        Nothing

从错误来看,我怀疑我对展开和包装 URI 片段的直觉是错误的。我哪里错了?什么是正确的直觉?

非常感谢您的帮助!

最简单的解决方案是将 monad 彼此嵌套,例如:

-- One instance for MonadThrow is Maybe, so this is a possible type signature
-- uriGen :: Gen (Maybe URI.URI)
uriGen :: MonadThrow m => Gen (m URI.URI)
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen -- (:: Gen Text), a simple generator for printable text. 
    let uri = do
          scheme <- URI.mkScheme sc
          host <- URI.mkHost $ (hostName <> "." <> tld)
          return $ URI.URI
                   { uriScheme = Just scheme
                   , uriAuthority = Right (URI.Authority Nothing host Nothing)
                   , uriPath = Nothing  
                   , uriQuery = []
                   , uriFragment = Nothing
                   }

    return uri

现在 uri 变量被解释为相对于 Gen monad 的纯值,并且 MonadThrow 将作为单独的层包装在其中。

如果你想让它重试直到成功,你可以按照moonGoose的建议使用suchThatMap。例如像这样:

uriGen' :: Gen URI.URI
uriGen' = suchThatMap uriGen id

这是有效的,因为 suchThatMap 有类型

suchThatMap :: Gen a -> (a -> Maybe b) -> Gen b

所以当你给它恒等函数作为第二个参数时,它就变成了

\x -> suchThatMap x id :: Gen (Maybe b) -> Gen b

与上面的类型匹配:uriGen :: Gen (Maybe URI.URI).


编辑: 在评论中回答您的问题:

MonadThrow 是 class 类型,它是 Monad 的超 class(参见 documentation)。你写的相当于


uriGen :: Gen URI.URI
uriGen = do
    sc <- elements ["https", "http", "ftps", "ftp"]
    tld <- elements [".com", ".org", ".edu"]
    hostName <- nonEmptySafeTextGen
    scheme <- URI.mkScheme sc
    host <- URI.mkHost $ (hostName <> "." <> tld)
    URI.URI (Just scheme) (Right (URI.Authority Nothing host Nothing)) Nothing [] Nothing

换句话说,do 的嵌套没有任何效果,它试图解释 Gen monad 中的所有内容。由于 Gen 不在 list of instances for MonadThrow 中,您会收到有关该错误的抱怨。

您可以在 ghci:

中使用 :i 检查一个类型实现了哪些实例以及哪些类型实现了一个类型 class
Prelude Test.QuickCheck> :i Gen
newtype Gen a
  = Test.QuickCheck.Gen.MkGen {Test.QuickCheck.Gen.unGen :: Test.QuickCheck.Random.QCGen
                                                            -> Int -> a}
    -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Applicative Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Functor Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Monad Gen -- Defined in ‘Test.QuickCheck.Gen’
instance [safe] Testable prop => Testable (Gen prop)
  -- Defined in ‘Test.QuickCheck.Property’