7

I want to implement the regular applicative instance for lists, using my customly defined list:

import Control.Monad

import Test.QuickCheck
import Test.QuickCheck.Checkers
import Test.QuickCheck.Classes

data List a =
  Nil
  | Cons a (List a)
  deriving (Eq, Ord, Show)


instance Functor List where
  fmap f (Cons x xs) = Cons (f x) (fmap f xs)
  fmap f Nil = Nil


instance Applicative List where
  pure x = Cons x Nil
  (<*>) Nil _ = Nil
  (<*>) _ Nil = Nil
  (<*>) (Cons f fs) xs = (+++) (fmap f xs) (fs <*> xs)

(+++) :: List a -> List a -> List a
(+++) (Cons x Nil) ys = Cons x ys
(+++) (Cons x xs) ys = Cons x xs'
  where xs' = (+++) xs ys

instance Arbitrary a => Arbitrary (List a)  where
  arbitrary = sized go
    where go 0 = pure Nil
          go n = do
            xs <- go (n - 1)
            x  <- arbitrary
            return (Cons x xs)

instance (Eq a) => EqProp (List a) where
  (=-=) = eq

main = do
  let trigger = undefined :: List (Int, String, Int)
  quickBatch $ applicative trigger

My code passes all the applicative tests in Checkers except one, the composition law. No error occurs when testing the composition law, it just never finishes.

Does my code recur eternally in some way I am unable to see, or is it just veeery slow for testing the compositon law?

This is the error message I get if I control-c during the Checkers execution:

applicative:
  identity:     +++ OK, passed 500 tests.
  composition:  *** Failed! Exception: 'user interrupt' (after 66 tests): 
Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> Nil))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> (Cons <function> Nil))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
Cons (-61) (Cons (-24) (Cons 56 (Cons (-10) (Cons 28 (Cons 5 (Cons (-5) (Cons 33 (Cons 18 (Cons 47 (Cons 43 (Cons 43 (Cons (-58) (Cons 35 (Cons (-52) (Cons (-52) (Cons (-41) (Cons 3 (Cons (-7) (Cons (-53) (Cons (-22) (Cons (-20) (Cons (-12) (Cons 46 (Cons (-53) (Cons 35 (Cons (-31) (Cons (-10) (Cons 43 (Cons (-16) (Cons 47 (Cons 53 (Cons 22 (Cons 8 (Cons 1 (Cons (-64) (Cons (-39) (Cons (-57) (Cons 34 (Cons (-31) (Cons 20 (Cons (-39) (Cons (-47) (Cons (-59) (Cons 15 (Cons (-42) (Cons (-31) (Cons 4 (Cons (-62) (Cons (-14) (Cons (-24) (Cons 47 (Cons 42 (Cons 61 (Cons 29 (Cons (-25) (Cons 30 (Cons (-20) (Cons 16 (Cons (-30) (Cons (-38) (Cons (-7) (Cons 16 (Cons 19 (Cons 20 Nil))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))))
  homomorphism: +++ OK, passed 500 tests.
  interchange:  +++ OK, passed 500 tests.
  functor:      +++ OK, passed 500 tests.

If one of the functions is slow, I guess it is the (+++), but I do not know how GHC executes code well enough to understand why.

Update:

The composition law is:

pure (.) <*> u <*> v <*> w = u <*> (v <*> w)

Which I can show works with my code for simple examples:

Cons (+1) Nil <*> (Cons (*2) Nil <*> Cons 1 (Cons 2 (Cons 3 Nil)))

and

pure (.) <*> Cons (+1) Nil <*> Cons (*2) Nil <*> Cons 1 (Cons 2 (Cons 3 Nil))

Both give the same result, so why the composition law never ends has me stumped. Might this be a problem with the checkers library?

The Unfun Cat
  • 29,987
  • 31
  • 114
  • 156
  • 2
    Maybe the size of the lists you end up generating is too large? What happens if you wrap the generators with [resize](http://hackage.haskell.org/package/QuickCheck-2.8.2/docs/Test-QuickCheck.html#v:resize), specifying a small size? – danidiaz May 19 '16 at 19:10
  • 3
    Using `List (Bool,Bool,Bool)` it completed for me in about 5 mins. – ErikR May 19 '16 at 19:45
  • If there are no solutions, I'll just accept an answer with a working applicative instance. – The Unfun Cat May 20 '16 at 10:51

1 Answers1

2

My first thought was that go was getting a negative argument and looping. However, when modifying it to use trace and throw an error if n < 0, I found that it's a lot simpler: your code is just really slow.

Here's the part I modified (go' was used for tracing, but I short circuited it for benchmarking):

import Debug.Trace

(+++) :: List a -> List a -> List a
{-# INLINE (+++) #-}
(+++) (Cons x Nil) ys = Cons x ys
(+++) (Cons x xs) ys = Cons x xs'
  where xs' = (+++) xs ys

maxListSize = 10

instance Arbitrary a => Arbitrary (List a)  where
  arbitrary = sized go''
    where
      go'' n = go' $ mod n maxListSize
      go' n = if n < 0 then error ("bad n:" ++ show n) else trace (show n ++ " , ") $ go n
      go 0 = pure Nil
      go n = do
        xs <- go' (n - 1)
        x  <- arbitrary
        return (Cons x xs)

Checking the trace for some sort of infinite loop, I found that things never stopped progressing, n kept decreasing then popping back up for the next test. It just took seconds for a single test when it slowed down. Remember you're trying to run 500 of each test.

My benchmarks aren't rigorous, but here's what I got (x is modulus, in range [1..18]):

Time Plot (x is modulus, y is seconds)

Quick regression found 5.72238 - 2.8458 x + 0.365263 x^2. When I ran the trace, n kept increasing. Although I'm not sure how the tests are being run, if it increases n each test, then n would get up to 500.

The formula isn't really fair, but let's assume it's a decent bound. (I think it should be since the algorithm is O(n^2).)

Then running all the tests would take roughly 25 hours, on my machine.

P.S. Since all the tests pass for reasonable bounds on n and I can't find a bug, I think your code is correct.

Michael Klein
  • 319
  • 4
  • 10
  • Your answer is good, thanks. Is it hard to find out which part of my code is slow? I think my code is very straightforward and do not see another way to do it. If you have an applicative instance for list that works I'd appreciate if you could add it since I could not find one on SO. And when you say my code is `O(n^2)`, do you mean `O(|functions| * |elements|)` from `(<*>) functions elements` or is it really `O(|elements|^2)`? – The Unfun Cat May 22 '16 at 04:56
  • 1
    `O(|fs = functions| * |es = elements|)`, because `fmap f x` is `O(| x |)`, `x +++ y` is `O(| x |)`, `O(fs <*> xs) = O(| xs |) + O(tail fs <*> xs)`. I played around with several optimizations and the following cut the time by more than `50%`: inlining `fmap, (<*>), (+++), arbitrary`. If you're curious why `Applicative []` is so much faster, look at the source for `GHC.Base` and [this discussion](http://comments.gmane.org/gmane.comp.lang.haskell.libraries/23298). Somewhere in the source is a `Note: [List comprehensions and inlining]`, but I haven't found it. – Michael Klein May 22 '16 at 17:58
  • Keeping this open until Friday, and will give the bounty if no even better answers. Thanks again. – The Unfun Cat May 23 '16 at 06:19