3

This question is related to my other question about smallCheck's Test.SmallCheck.Series class. When I try to define an instance of the class Serial in the following natural way (suggested to me by an answer by @tel to the above question), I get compiler errors:

data Person = SnowWhite | Dwarf Int
instance Serial Person where ...

It turns out that Serial wants to have two arguments. This, in turn, necessitates a some compiler flags. The following works:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
import Test.SmallCheck
import Test.SmallCheck.Series
import Control.Monad.Identity

data Person = SnowWhite | Dwarf Int

instance Serial Identity Person where
        series = generate (\d -> SnowWhite : take (d-1) (map Dwarf [1..7]))

My question is:

  1. Was putting that Identity there the "right thing to do"? I was inspired by the type of the Test.Series.list function (which I also found extremely bizarre when I first saw it):

    list :: Depth -> Series Identity a -> [a]
    

    What is the right thing to do? Will I be OK if I just blindly put Identity in whenever I see it? Should I have put something like Serial m Integer => Serial m Person instead (that necessitates some more scary-looking compiler flags: FlexibleContexts and UndecidableInstances at least)?

  2. What is that first parameter (the m in Serial m n) for?

    Thank you!

Community
  • 1
  • 1

1 Answers1

4

I'm just an user of smallcheck and not a developer, but I think the answer is

1) Not really. You should leave it polymorphic, which you can do without the said extensions:

{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses #-}
import Test.SmallCheck
import Test.SmallCheck.Series
import Control.Monad.Identity

data Person = SnowWhite | Dwarf Int deriving (Show)

instance (Monad m) => Serial m Person where
        series = generate (\d -> SnowWhite : take (d-1) (map Dwarf [1..7]))

2) Series is currently defined as

newtype Series m a = Series (ReaderT Depth (LogicT m) a)

which means that mis the base monad for LogicT which is used to generate the values in the series. For example, writing IO in place of m would allow IO actions to happen while generating the series.

In SmallCheck, m appears also in the Testable instance declarations, such as instance (Serial m a, Show a, Testable m b) => Testable m (a->b). This has the concrete effect that the pre-existing driver functions such as smallCheck :: Testable IO a => Depth -> a -> IO () cannot be used if you only have instances for Identity.

In practice, you could make use of this fact by writing a custom driver function which interleaves some monadic effect like logging of the generated values (or some such) inside the said driver.

It might also be useful for other things which I'm not aware of.

aleator
  • 4,436
  • 20
  • 31
  • Thank you very much for a great answer and the explanation! Your example works, it should really be in the documentation. Though, I must confess, I am angry that I must understand what `Series (ReaderT Depth (LogicT m) a)` means just to use a library for a concrete purpose. – user21952-is-a-great-name May 15 '13 at 18:09
  • 1
    You don't really need to care about `LogicT` to use smallcheck. – aleator May 15 '13 at 18:13
  • Well, maybe I don't need *more* details. However, because of it, I would never be able to guess what I'm supposed to do to make my code work without your help. – user21952-is-a-great-name May 15 '13 at 18:20
  • It's definitely deficiency in the documentation---SmallCheck is a less popular library than QuickCheck---but to answer quickly: `ReaderT Depth (LogicT m) a` is *very* similar to `Depth -> [a]`. The only change is it allows monadic effects from a monad `m` to be interleaved into your list. In particular, logic effects which allow higher-level descriptions of branching and conditional series generation. We can eliminate these effects by setting `m ~ Identity` and then they're exactly equal: `Serial Identity a ~ Depth -> [a]`. – J. Abrahamson May 15 '13 at 20:32
  • To emphasize this, here's the definition of `generate f = getDepth >>= \d -> msum $ map return $ f d`. In other words, it uses the `ReaderT` monad to get the `Depth`, passes it to your `Depth -> [a]` function, maps `return` over it to get a bunch of benign monadic effects, then combines the results. – J. Abrahamson May 15 '13 at 21:11