Can GHC warn if class instance is a loop?

后端 未结 5 1526
长情又很酷
长情又很酷 2021-01-17 11:54

Real World Haskell has this example:

class BasicEq3 a where
    isEqual3 :: a -> a -> Bool
    isEqual3 x y = not (isNotEqual3 x y)

    isNotEqual3 ::         


        
相关标签:
5条回答
  • 2021-01-17 12:09

    No, I'm afraid GHC doesn't do anything like that. Also that isn't possible in general.

    You see, the methods of a type class could be mutually recursive in a useful way. Here's a contrived example of such a type class. It's perfectly fine to not define either sumOdds or sumEvens, even though their default implementations are in terms of each other.

    class Weird a where
        measure :: a -> Int
    
        sumOdds :: [a] -> Int
        sumOdds [] = 0
        sumOdds (_:xs) = sumEvens xs
    
        sumEvens :: [a] -> Int
        sumEvens [] = 0
        sumEvens (x:xs) = measure x + sumOdds xs
    
    0 讨论(0)
  • 2021-01-17 12:21

    I think it's perfectly fine for GHC to issue a warning in case of an "unbroken" cyclic dependency. There's even a ticket along those lines: http://hackage.haskell.org/trac/ghc/ticket/6028

    Just because something is "undecidable" doesn't mean no instance of the problem can be solved effectively. GHC (or any other Haskell compiler) already has quite a bit of the information it needs, and it'd be perfectly possible for it to issue a warning if the user is instantiating a class without "breaking" the cyclic dependency. And if the compiler gets it wrong in the rare cases as exemplified in previous posts, then the user can have a -nowarnundefinedcyclicmethods or a similar mechanism to tell GHC to be quiet. In nearly every other case, the warning will be most welcome and would add to programmer productivity; avoiding what's almost always a silly bug.

    0 讨论(0)
  • 2021-01-17 12:25

    In my personal opinion, the defaulting mechanism is unnecessary and unwise. It would be easy for the class author to just provide the defaults as ordinary functions:

    notEq3FromEq3 :: (a -> a -> Bool) -> (a -> a -> Bool)
    notEq3FromEq3 eq3 = (\x y -> not (eq3 x y))
    
    eq3FromNotEq3 :: (a -> a -> Bool) -> (a -> a -> Bool)
    eq3FromNotEq3 ne3 = (\x y -> not (ne3 x y))
    

    (In fact, these two definitions are equal, but that would not be true in general). Then an instance looks like:

     instance BasicEq3 Bool where
       isEqual3 True True = True
       isEqual3 False False = True
       isEqual3 _ _ = False
    
       isNotEqual3 = notEq3FromEq3 isEqual3
    

    and no defaults are required. Then GHC can warn you if you don't provide the definition, and any unpleasant loops have to be explicitly written by you into your code.

    This does remove the neat ability to add new methods to a class with default definitions without affecting existing instances, but that's not so huge a benefit in my view. The above approach is also in principle more flexible: you could e.g. provide functions that allowed an Ord instance to choose any comparison operator to implement.

    0 讨论(0)
  • 2021-01-17 12:26

    No, there isn't, since if the compiler could make this determination, that would be equivalent to solving the Halting Problem. In general, the fact that two functions call each other in a "loop" pattern is not enough to conclude that actually calling one of the functions will result in a loop.

    To use a (contrived) example,

    collatzOdd :: Int -> Int
    collatzOdd 1 = 1
    collatzOdd n = let n' = 3*n+1 in if n' `mod` 2 == 0 then collatzEven n'
                                       else collatzOdd n'
    
    collatzEven :: Int -> Int
    collatzEven n = let n' = n `div` 2 in if n' `mod` 2 == 0 then collatzEven n'
                                            else collatzOdd n'
    
    collatz :: Int -> Int
    collatz n = if n `mod` 2 == 0 then collatzEven n else collatzOdd n
    

    (This is of course not the most natural way to implement the Collatz conjecture, but it illustrates mutually recursive functions.)

    Now collatzEven and collatzOdd depend on each other, but the Collatz conjecture states that calling collatz terminates for all positive n. If GHC could determine whether a class that had collatzOdd and collatzEven as default definitions had a complete definition or not, then GHC would be able to solve the Collatz conjecture! (This is of course not a proof of the undecideability of the Halting Problem, but it should illustrate why determining whether a mutually recursive set of functions is well-defined is not at all as trivial as it may seem.)

    In general, since GHC cannot determine this automatically, documentation for Haskell classes will give the "minimal complete definition" necessary for creating an instance of a class.

    0 讨论(0)
  • 2021-01-17 12:28

    I don't think so. I worry that you're expecting the compiler to solve the halting problem! Just because two functions are defined in terms of each other, doesn't mean it's a bad default class. Also, I've used classes in the past where I just needed to write instance MyClass MyType to add useful functionality. So asking the compiler to warn you about that class is asking it to complain about other, valid code.

    [Of course, use ghci during development and test every function after you've written it! Use HUnit and/or QuickCheck, just to make sure none of this stuff ends up in final code.]

    0 讨论(0)
提交回复
热议问题