haskell-hedgehog icon indicating copy to clipboard operation
haskell-hedgehog copied to clipboard

Proposal: port of quickcheck's within, total, Gen.tuple2

Open dcastro opened this issue 5 years ago • 12 comments

I was thinking of porting these functions from QuickCheck over to Hedgehog, and wanted to know what the maintainers think first.

Hedgehog module:

-- A port of quickcheck's `total`
-- Similar to hedgehog's `eval`, but evaluates to NF instead of WHNF
-- https://hackage.haskell.org/package/QuickCheck-2.14/docs/Test-QuickCheck.html#v:total
evalNF :: (MonadTest m, NFData a, HasCallStack) => a -> m a

-- A port of quickcheck's `within`
-- https://hackage.haskell.org/package/QuickCheck-2.14/docs/Test-QuickCheck.html#v:within
within :: (MonadTest m, HasCallStack) => Int -> m a -> m a

Hedgehog.Gen module:


tuple2 :: MonadGen m => m a -> m b -> m (a, b)
tuple3 :: MonadGen m => m a -> m b -> m c -> m (a, b, c)
-- All the way up to tuple10, possibly?

dcastro avatar May 17 '20 14:05 dcastro

Thank you, @dcastro :rocket: Let's do the within one first, if you wish :eyes: — On the others, for example, the tuples are trivial, so perhaps not really worth the effort(?)

tuple2 = liftM2 (,)
tuple3 = liftM3 (,,)

And for the total one perhaps we need a plan, e.g. should we port it as-is

total x = eval (rnf x)

or perhaps consider @sjakobi's suggestion in https://github.com/nick8325/quickcheck/issues/265, plus we might need to pickup a better name for it.

I've never had to use QC's within, and total, so others suggestions are more than welcome.

moodmosaic avatar May 18 '20 06:05 moodmosaic

tuples

Ah, I keep forgetting about liftA/M :sweat_smile: Yea I agree, not worth the effort then!

total

Good point, we should definitely give the user a better error message. Maybe catch the exception and use something similar to Hedgehog.Internal.failException? E.g.

total :: (MonadTest m, NFData a, HasCallStack) => a -> m ()
total x =
  either (withFrozenCallStack catchException) pure (tryEvaluate (rnf x))
  where
    catchException (SomeException ex) =
      failWith Nothing $ unlines [
          "━━━ Value was not total ━━━"
        , "━━━ Exception (" ++ show (typeOf ex) ++ ") ━━━"
        , List.dropWhileEnd Char.isSpace (displayException ex)
        ]

within

I'll have a go at implementing within. So far I've only been able to implement it in a way that it takes an IO a:

within :: (MonadTest m, MonadIO m, HasCallStack) => Int -> IO a -> m a
within microseconds io = withFrozenCallStack $
  liftIO (timeout microseconds io) >>= \case
    Just x -> return x
    Nothing -> failWith Nothing $
      "━━━ Operation did not complete within " ++ show microseconds ++ " microseconds ━━━"

But ideally, I would like it to take a generalized MonadTest m => m a as an input instead, so that it could be used with other combinators.

within :: (MonadTest m, MonadIO m, HasCallStack) => Int -> m a -> m a

-- usage
property $ within 1000 $ total (f args)

I think I'll have to dive deeper into hedgehog in order to make this work (I'm assuming such an implementation is even possible :eyes: what do you think?). If I get stuck, I might post again here in search of guidance.

dcastro avatar May 18 '20 20:05 dcastro

I think I solved within in https://github.com/hedgehogqa/haskell-hedgehog/issues/211

HuwCampbell avatar May 19 '20 02:05 HuwCampbell

@HuwCampbell oh awesome! Do you want to go ahead and submit a PR with that? Otherwise I'm ok doing it too.

On a related topic, I'm not sure what the best name would be for total/evalNF...

dcastro avatar May 19 '20 22:05 dcastro

On a related topic, I'm not sure what the best name would be for total/evalNF...

I've thought about it a bit, but haven't come up with anything better than total or just nf.

sjakobi avatar May 20 '20 17:05 sjakobi

@moodmosaic so far we have total, evalNF and nf on the table, do any of these sound reasonable? Personally, I think evalNF pairs well with the existing eval for whnf, but I'm ok with way.

@HuwCampbell How should we proceed regarding within? I wouldn't mind taking over if you don't have the time.

dcastro avatar May 23 '20 19:05 dcastro

I can put something up.

HuwCampbell avatar May 24 '20 08:05 HuwCampbell

@dcastro

so far we have total, evalNF and nf on the table, do any of these sound reasonable?

Perhaps we should stick with the QC terminology for this one (total)? If not, perhaps something with a mix of eval and deepseq, e.g. force, deepeval, evaldeep 🤔 /cc @jacobstanley

moodmosaic avatar May 26 '20 12:05 moodmosaic

@moodmosaic Let's go with total then :)

One more suggestion if I may, something similar to quickcheck's implication operator ( ==>):

imply :: Monad m => Bool -> PropertyT m a -> PropertyT m a
imply False _ = discard
imply True prop = prop

dcastro avatar May 26 '20 16:05 dcastro

@moodmosaic Let's go with total then :)

I discussed about this with @jacobstanley a few times now (naming can be so hard!)—evalNF (your first suggestion) would be better because it also matches with the NFData typeclass constraint. :rocket:

(I apologize for going back and forth on that one, it's just that once we release it, it's there forever...)

moodmosaic avatar May 28 '20 21:05 moodmosaic

@dcastro, on imply/==>, have you tried guard?

moodmosaic avatar May 28 '20 21:05 moodmosaic

@moodmosaic No problem at all, it's understandable :) Alright, I'll create a PR then.

On guard, I was under the false impression that it would fail the test instead of discarding it, but seems I was wrong :D that's good to know, thanks!

dcastro avatar May 29 '20 07:05 dcastro