There's a fine tradition of including 'smart constructor' methods in an interface (class
):
class Collection c a where
empty :: c a
singleton :: a -> c a
-- etc
It would be nice to supply them as pattern-synonyms:
{-# LANGUAGE PatternSynonyms #-}
instance Ord a => Collection [] a where
empty = []
singleton x = [x]
pattern PEmpty = []
pattern PSingleton x = [x]
But you can't put PatSyns inside classes. I've got something working, but it seems an awful lot of ugly code. Can anyone suggest ways to clean it up? (Specific uglinesses itemised below.)
{-# LANGUAGE ViewPatterns #-}
pattern PEmpty :: (Eq (c a), Collection c a) => c a
pattern PEmpty <- ((==) empty -> True) where
PEmpty = empty
pattern PSingleton :: (Ord a, Collection c a) => a -> c a
pattern PSingleton x <- (isSingleton -> Just x) where
PSingleton x = singleton x
-- needs an extra method in the class:
isSingleton :: c a -> Maybe a
-- instance ...
isSingleton [x] = Just x
isSingleton _ = Nothing
- It's annoying that these need to be explicitly bi-directional patterns; especially since the 'under
where
' line is so directly comparable to the instance overload. - For the
empty
pattern I have to introduce an explicit(==)
test and supportingEq
constraint -- to achieve a simple pattern match. (I suppose I could call anisEmpty
method.) - For the
singleton
pattern, I've avoided an explicit(==)
test, but needed to double-up on a method in the class. - I really do not like
ViewPatterns
; I'd hope PatSyns could provide some way to avoid them.
CodePudding user response:
If you could put pattern synonyms in the class instance, then your solution would be even simpler: you wouldn't need empty
or singleton
in the class at all as they would be definable in terms of PEmpty
and PSingleton
. Alas, as you know, this is impossible in the current GHC.
As is, you can only define functions and types in your class, and without access to constructors (like []
and :
in your list example), you need two functions to define a pattern synonym: one for extracting data from the type and one for embedding into it.
As to your specific points about ugliness, some are unavoidable, but for others, there may be a slightly cleaner approach.
It's annoying that these need to be explicitly bi-directional patterns; especially since the 'under
where
' line is so directly comparable to the instance overload.
Alas, this one is unavoidable. Without access to constructors in the class, you need to use explicitly bidirectional patterns.
For the
empty
pattern I have to introduce an explicit(==)
test and supportingEq
constraint -- to achieve a simple pattern match. (I suppose I could call anisEmpty
method.)
I personally think your code would be cleaner with an isEmpty
method. For what it's worth, you can use a default signature if you like, like:
class Collection c a where
...
isEmpty :: c a -> Bool
default isEmpty :: Eq (c a) => c a -> Bool
isEmpty = (==) empty
For the
singleton
pattern, I've avoided an explicit(==)
test, but needed to double-up on a method in the class.
You say this like you had a choice, but I don't think you can write this pattern with just an equality test. You're trying to extract a value from an unknown collection, and I don't see how you can do that without support from your class. An extractSingleton
method seems quite reasonable, just as headMay :: [a] -> Maybe a
(a safe version of head
) is quite reasonable.
I really do not like
ViewPatterns
; I'd hope PatSyns could provide some way to avoid them.
This is another "alas" moment. I'm sorry you don't like ViewPatterns
, but they're pretty much the only way to write bidirectional pattern synonyms.
The big thing you didn't mention is the issue around complete pattern matches. You may find that even when you have these new patterns, they'll throw all sorts of warnings about non-exhaustive pattern matches. You probably want to define a more general "cons"-like pattern for destructing your collections. Then, you can add a COMPLETE
pragma for PEmpty
and PCons
.