In this talk around the 1:20 mark, Edward Kmett mentions the lack of "type class backtracking" in Haskell. Consider the problem of "set equality" (where order and multiplicity are disregarded) implemented on lists:
equals :: [a] -> [a] -> Bool
By the nature of type classes I cannot provide an inefficient O(n²) comparison if all we have is Eq a
but efficiently compare the lists in O(n log n) if we have Ord a
.
I did understand why such a facility would be problematic. At the same time, Edward says that there are "tricks you could play" mentioning for instance type families.
Hence my question is, what would be a workaround to achieve the same effect:
- an (inefficient) default implementation is provided
- if the user can provide some additional information about the type, we "unlock" a more efficient implementation
This workaround does not necessarily need to use type classes.
Edit: Some people suggested simply providing equals
and efficientEquals
as two distinct methods. In general I agree that this is the more Haskell-idiomatic approach. However, I am not convinced that this is always possible. For instance, what if the equals method above is itself part of a type class:
class SetLike s where
equals :: Eq a => s a -> s a -> Bool
Assume that this class has been provided by someone else so I cannot simply add a new function to the typeclass. I now want to define the instance for []
. I know that []
can always provide an implementation of equals
no matter what constraints there are on a
but I cannot tell my instance to use the more efficient version if more information about a
is available.
Maybe I could wrap the list in a newtype and tag it with some additional type information?
efficientEquals :: (Ord a) => [a] -> [a] -> Bool
andequals :: (Eq a) => [a] -> [a] -> Bool
. - Shoe(==)
does not require O(n^2) on lists. - chiOverlappingInstances
, or closed type families that can be used to get something like what you're looking for in some cases (though not really here; unless you're okay with, say, enumerating/duplicating all types withOrd
instances you care about). See the "advanced overlap" wiki page. The "extra method trick" might also be relevant here. - jberrymanOrd a
does not hold. The type class system in Haskell is strongly designed to make sure it never commits to the negative of a constraint; this property is at the heart of a lot of the consistency guarantees it provides, because with separate compilation of modules GHC can never tell the difference between "no instance exists" and "no instance is in scope". - Benequals
that works with both Eq and Ord instances, but I can't think of any way that wouldn't just complain about ambiguous type variables when called on things are instances of Ord. You'd end up having to manually say(equals :: Ord a => [a] -> [a] -> Bool) xs ys
at every call site. And if you're willing to manually declare which version of equals you want to call, they might as well have different names so it's easier to do so. - Ben