代码之家  ›  专栏  ›  技术社区  ›  Marc

使用typeclass约束快速检查并报告生成的值?

  •  0
  • Marc  · 技术社区  · 7 年前

    我正在为一个象棋游戏做一个基于属性的测试。我已经设置了下列类型

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

    它检查给定坐标是否包含冲突或越界。

    现在我有了一个函数,可以为一个骑士生成允许的动作集,如下所示

    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                             
    
    

    以及任意坐标的hasCheck类的一个实例

    instance HasCheck Gen where                                                      
        isCollision _ =                                                              
             Quickcheck.arbitrary                                                    
    
    

    因此,在测试之后,我想确保生成的移动集是所有可能移动的适当子集。

    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
    

    当然,这失败了,因为这可能是因为骑士不能移动到任何地方,这意味着它不是一个适当的子集,而是同一个集合。然而,报告的错误并没有特别有帮助。

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

    这是因为QuickCheck不报告IScollision的生成值。因此,我想知道,如何让QuickCheck报告 isCollision ?

    0 回复  |  直到 7 年前
        1
  •  0
  •   Marc    7 年前

    好吧,我觉得这应该可以用另一种方式解决。然而,我提出了以下解决方案,其灵感来自 handler pattern .

    我将hasCheck类型类更改为记录,如下所示:

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

    然后将所有代码重构为使用handle而不是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
    

    这一点的缺点是,我担心对于有状态的代码,在传递一个过时的句柄(即具有多个真理源)时很容易引入错误。一个优势是,这可能更容易让哈斯克尔的新手理解。现在,我们可以使用QuickCheck的函数typeclass模拟函数,并将其作为参数传递,以生成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)
    

    现在,用一个反例来说,这完全失败了:

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