Search code examples
haskellurimonadsquickcheckdo-notation

Problem with nested monads while writing a simple QuickCheck URL generator


Another newbie question that probably results from me not having grasped Monadic do in Haskell: I want to write a simple QuickCheck generator for well-formed URIs, using the Text.URI type from the modern-uri package. To my understanding, there are two types of monads involved here: MonadThrow for error handling upon URI construction, and Gen from QuickCheck.

Here is my attempt to implement the generator. It does not type check:

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

My understanding is that the outer do block pertains to the Gen monad while the inner one handles MonadThrow. I attempt to unwrap the Text pieces from their Gen and then use the unwrapped Text to build up URI pieces, unwrap them from their MonadThrow, then reassemble the entire URI, and finally wrap it in a new Gen.

However, I get the following type-check error:

    • 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

From the error, I suspect that my intuition about unwrapping and wrapping the URI pieces is wrong. Where do I err? What would be the right intuition?

Thanks very much for your help!


Solution

  • The easiest solution would be to nest the monads within each other, for example like this:

    -- 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
    

    Now the uri variable is interpreted as a pure value with respect to the Gen monad and the MonadThrow will be wrapped as a separate layer inside it.

    If you want it to retry until it succeeds, you can use suchThatMap as moonGoose suggested. For example like this:

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

    This works because suchThatMap has type

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

    so when you give it the identity function as a second argument, it becomes

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

    which matches the type above: uriGen :: Gen (Maybe URI.URI).


    EDIT: To answer your question in the comments:

    MonadThrow is a typeclass that is a superclass of Monad (see documentation). What you wrote is equivalent to

    
    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
    

    In other words, the nesting of do has no effect and it tries to interpret everything in the Gen monad. Since Gen is not in the list of instances for MonadThrow, you get the error complaining about that.

    You can check which instances a type implements and which types implements a type class using :i in ghci:

    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’