Ulrich Schuster
Ulrich Schuster

Reputation: 1906

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!

Upvotes: 2

Views: 124

Answers (1)

Hjulle
Hjulle

Reputation: 2615

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’

Upvotes: 1

Related Questions