2

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!

Ulrich Schuster
  • 1,670
  • 15
  • 24
  • Perhaps you could use `suchThatMap` and the `MonadThrow` instance for `Maybe` – moonGoose May 07 '20 at 20:16
  • 1
    An important detail: What do you want the program to do on failure? Should it retry until you get a valid URI or should it return a value indicating failure? – Hjulle May 07 '20 at 23:45

1 Answers1

1

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’
Hjulle
  • 2,471
  • 1
  • 22
  • 34
  • 1
    Thanks very much for your help. For my understanding: Aside from the question what to do on failure, my type error resulted because I did use a monadic binding for `url` instead of a `let` binding? Because the outer monad is `Gen` instead of `MonadThrow`, that monadic binding doesn't type-check? – Ulrich Schuster May 08 '20 at 08:37
  • 1
    @UlrichSchuster I've updated my answer now. Does that clarify it? Yes, you're mostly right. The only detail is that `MonadThrow` is not just a single `Monad` but a class of monads, which include `Maybe`, `Either` and `IO`, but not `Gen`. – Hjulle May 08 '20 at 08:54
  • @UlrichSchuster If you look at how `do`-notation desugars, it might help you get an intuition for why it is equivalent: https://en.wikibooks.org/wiki/Haskell/do_notation – Hjulle May 08 '20 at 08:59