typeclass challenge: having both variadic arguments and results
Asked Answered
V

2

9

While writing some Arbitrary instances, I implemented a couple of functions with the following quite mechanical pattern:

type A = Arbitrary -- to cut down on the size of the annotations below
shrink1 :: (A a          ) => (a           -> r) -> (a           -> [r])
shrink2 :: (A a, A b     ) => (a -> b      -> r) -> (a -> b      -> [r])
shrink3 :: (A a, A b, A c) => (a -> b -> c -> r) -> (a -> b -> c -> [r])

shrink1 f a     = [f a'     | a' <- shrink a]
shrink2 f a b   = [f a' b   | a' <- shrink a] ++ [f a b'   | b' <- shrink b]
shrink3 f a b c = [f a' b c | a' <- shrink a] ++ [f a b' c | b' <- shrink b] ++ [f a b c' | c' <- shrink c]

I wrote out these functions by hand up to shrink7, and that seems to be sufficient for my needs. But I can't help but wonder: can this reasonably be automated? Bonus points for a solution that:

  • allows for shrink0 f = []
  • generates all the shrinkers
  • has loads of typeclass hackery, I love that
  • skips the scary extensions like incoherent/undecidable/overlapping instances
  • lets me have my cake and eat it, too: doesn't require me to uncurry f when passing it in or curry the application shrinkX f when applying it to a, b, and c
Vallievalliere answered 17/8, 2012 at 23:36 Comment(2)
@BenMillwood I'll be scared by whatever I want to be scared by! ;-)Vallievalliere
Yeah, sure, go ahead. I just feel like mentioning it in the same breath as incoherent does it a bit of a disservice.Selfseeking
C
9

This compiles, I hope it works:

{-# LANGUAGE TypeFamilies #-}
import Test.QuickCheck

class Shrink t where
  type Inp t :: *
  shrinkn :: Inp t -> t
  (++*) :: [Inp t] -> t -> t

instance Shrink [r] where
  type Inp [r] = r
  shrinkn _ = []
  (++*) = (++) 

instance (Arbitrary a, Shrink s) => Shrink (a -> s) where
  type Inp (a -> s) = a -> Inp s
  shrinkn f a = [ f a' | a' <- shrink a ] ++* shrinkn (f a)
  l ++* f = \b -> map ($ b) l ++* f b

(++*) is only for implementing shrinkn.

Sorry for the relative lack of typeclass hackery. The [r] provides a nice stop condition for the type recursion, so hackery isn't needed.

Copywriter answered 18/8, 2012 at 0:35 Comment(2)
I really like the (++*) trick... and only needing TypeFamilies (it seems to work even without the totally unscary FlexibleInstances) is quite attractive, too.Vallievalliere
Ah, FlexibleInstances was for an older version without (++*).Copywriter
N
2

I doubt you can avoid scary extensions in this case, but otherwise:

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, TypeFamilies,
 UndecidableInstances, IncoherentInstances #-}

import Test.QuickCheck

class Shrinkable a r where
    shrinkn :: a -> r

instance (Shrinkable [a -> b] r) => Shrinkable (a -> b) r where
    shrinkn f = shrinkn [f]

instance (Arbitrary a, Shrinkable [b] r1, r ~ (a -> r1)) => Shrinkable [a -> b] r where
    shrinkn fs@(f:_) a =
        let fs' = [f a | f <- fs]
        in shrinkn $ fs' ++ [f a' | a' <- shrink a]

instance (r ~ [a]) => Shrinkable [a] r where
    shrinkn (_:vs) = vs

instance (r ~ [a]) => Shrinkable a r where
    shrinkn e = []

Here are a few Quickcheck properties to test against your example functions:

prop0 a = shrinkn a == []

prop1 a = shrink1 not a == shrinkn not a 

prop2 a b = shrink2 (++) a b == shrinkn (++) a b 

f3 a b c = if a then b + c else b * c 
prop3 a b c = shrink3 f3 a b c == shrinkn f3 a b c 
Novation answered 18/8, 2012 at 7:8 Comment(2)
Using QuickCheck to check the code we're writing to use QuickCheck... LOVE it!Vallievalliere
<benmachine> @quote quicksilver <lambdabot> quicksilver says: overlapping actually shatters the language into tiny inconsistent pieces, and incoherent files off the edges of the pieces so they don't even fit together any more.Selfseeking

© 2022 - 2024 — McMap. All rights reserved.