Search code examples
haskelltestingtypeclassquickcheck

Quickchecking with a typeclass constraint and reporting the generated values?


I'm trying to do a property-based test for a chess game. I have set up the following typeclass

class Monad m => HasCheck m where                                                   
    isCollision :: Coord -> m Bool                                                  

which checks if a given coordinate contains a collision or out of bounds.

Now I have a function that generates the moveset of allowed actions for a knight like the following

collisionKnightRule :: HasCheck m => Coord -> m (Set Coord)                      
collisionKnightRule =                                                            
    Set.filterM isCollision . knightMoveSet                                      


-- | Set of all moves, legal or not                                              
knightMoveSet :: Coord -> Set Coord                                              
knightMoveSet (x,y) =                                                            
    Set.fromList                                                                 
        [ (x+2,y-1),(x+2,y+1),(x-2,y-1),(x-2,y+1)                                
        , (x+1,y-2),(x+1,y+2),(x-1,y-2),(x-1,y+2)                                
        ]                                                                        



knightMoves :: HasCheck m => Coord -> m (Set Coord)                              
knightMoves pos =                                                                
    do  let moveSet =                                                            
                knightMoveSet pos                                                
        invalidMoves <- collisionKnightRule pos                                        
        return $ Set.difference moveSet invalidMoves                             

and an instance for the HasCheck class for an arbitrary coordinate

instance HasCheck Gen where                                                      
    isCollision _ =                                                              
         Quickcheck.arbitrary                                                    

and so afterwards to test this I want to ensure that the generated moveset is a proper subset of all possible moves.

knightSetProperty :: Piece.HasCheck Gen                                          
    => (Int,Int)                                                                 
    -> Gen Bool                                                                  
knightSetProperty position =                                                     
    do  moves <- Piece.knightMoves position                                      
        return $ moves `Set.isProperSubsetOf` (Piece.knightMoveSet position)

-- ... later on

it "Knight ruleset is subset" $                                          
            quickCheck knightSetProperty

Of course this fails because it could be that the knight can't move anywhere, which would mean that it's not a proper subset but the same set. However the error reported is not particularly helpful

*** Failed! Falsifiable (after 14 tests and 3 shrinks):  
(0,0)

This is because quickcheck doesn't report the generated value of isCollision. Therefore I wonder, how can I make quickCheck report the generated value of isCollision?


Solution

  • Okay so I feel this should be solvable in another way. However I made the following solution that works inspired by the handler pattern.

    I changed the HasCheck typeclass to a record, as follows:

    data Handle = MakeHandle                                                                   
        { isCollision   :: Coord -> Bool                                                   
        }      
    
    

    and then refactored all the code to use handle instead of HasCheck.

    collisionKnightRule :: Handle -> Coord -> (Set Coord)                            
    collisionKnightRule handle =                                                     
        Set.filter (isCollision handle) . knightMoveSet                              
    
    
    -- | Set of all moves, legal or not                                              
    knightMoveSet :: Coord -> Set Coord                                              
    knightMoveSet (x,y) =                                                            
        Set.fromList                                                                 
            [ (x+2,y-1),(x+2,y+1),(x-2,y-1),(x-2,y+1)                                
            , (x+1,y-2),(x+1,y+2),(x-1,y-2),(x-1,y+2)                                
            ]                                                                        
    
    
    -- | Set of illegal moves                                                        
    knightRuleSet :: Handle -> Coord -> (Set Coord)                                  
    knightRuleSet =                                                                  
        collisionKnightRule                                                          
    
    
    knightMoves :: Handle -> Coord -> (Set Coord)                                    
    knightMoves handle pos =                                                         
        let                                                                          
            moveSet =                                                                
                knightMoveSet pos                                                    
    
            invalidMoves =                                                           
                knightRuleSet handle pos                                             
        in                                                                           
            Set.difference moveSet invalidMoves
    

    The disadvantage of this is I fear that for stateful code it can be easy to introduce an error where you pass in a handle that is out-of-date, I.E. having multiple sources of truths. An advantage is that this is probably easier for people new to Haskell to understand. We can now mock functions using the Quickcheck's Function typeclass and pass them as an argument to make a mockHandler:

    knightSetProperty ::                                                                 
        Fun (Int,Int) Bool                                                               
        -> (Int,Int)                                                                     
        -> Gen Bool                                                                      
    knightSetProperty (Fun _ isCollision) position =                
        let                                                                              
            handler = 
                Piece.MakeHandle isCollision                           
            moveSet =                                                                      
                Piece.knightMoves handler position                                       
        in                                                                               
            return $ moveSet `Set.isProperSubsetOf` (Piece.knightMoveSet position)
    

    Now this fails properly with a counterexample:

    *** Failed! Falsifiable (after 53 tests and 74 shrinks):     
    {_->False}
    (0,0)