I am writing a Haskell function which takes a list as input. That is, there's no reason it couldn't be a queue or dequeue, or anything that allows me to access its "head" and its "tail" (and check if it's empty). So the [a] input type seems too specific. But AFAIK there's no standard library typeclass that captures exactly this interface. Sure, I could wrap my function in a Data.Foldable.toList and make it polymorphic wrt Foldable, but that doesn't quite seem right (idiomatic).
Why is there no standard list type class? (And why is the "container" type class hierarchy in Haskell less developed than I think it should be?) Or am I missing something essential?
Type Classes are a language mechanism in Haskell designed to support general overloading in a principled way. They address each of the concerns raised above. They provide concise types to describe overloaded functions, so there is no expo- nential blow-up in the number of versions of an overloaded function.
One is of type (String,Int) , whereas the other is (Int,String) . This has implications for building up lists of tuples. We could very well have lists like [("a",1),("b",9),("c",9)] , but Haskell cannot have a list like [("a",1),(2,"b"),(9,"c")] . Which of these are valid Haskell, and why?
In Haskell, lists are a homogenous data structure. It stores several elements of the same type. That means that we can have a list of integers or a list of characters but we can't have a list that has a few integers and then a few characters.
An instance of a class is an individual object which belongs to that class. In Haskell, the class system is (roughly speaking) a way to group similar types. (This is the reason we call them "type classes"). An instance of a class is an individual type which belongs to that class.
A given algebraic datatype can be represented as its catamorphism, a transformation known as Church encoding. That means lists are isomorphic to their foldr
:
type List a = forall b. (a -> b -> b) -> b -> b
fromList :: [a] -> List a
fromList xs = \f z -> foldr f z xs
toList :: List a -> [a]
toList l = l (:) []
But foldr
also characterises Foldable
. You can define foldMap
in terms of foldr
, and vice versa.
foldMap f = foldr (mappend . f) mempty
foldr f z t = appEndo (foldMap (Endo . f) t) z
(It shouldn't be surprising that foldMap :: Monoid m => (a -> m) -> [a] -> m
characterises lists, because lists are a free monoid.) In other words, Foldable
basically gives you toList
as a class. Instances of Foldable
have a "path" through them which can be walked to give you a list; Foldable
types have at least as much structure as lists.
Regarding your misgivings:
It's not like
Foldable
has functionshead
/tail
/isEmpty
, which is what I would find more intuitive.
null :: Foldable t => t a -> Bool
is your isEmpty
, and you can define (a safe version of) head
straightforwardly with an appropriate choice of Monoid
:
head :: Foldable t :: t a -> Maybe a
head = getFirst . foldMap (First . Just)
tail
is kinda tricky in my opinion. It's not obvious what tail
would even mean for an arbitrary type. You can certainly write tail :: Foldable t => t a -> Maybe [a]
(by toList
ing and then unconsing), but I think any type T
for which tail :: T a -> Maybe (T a)
is defined would necessarily be structurally similar to lists (eg Seq
). Besides, in my experience, the vast majority of cases where you'd think you need access to a list's tail
turn out to be folds after all.
That said, abstracting over unconsable types is occasionally useful. megaparsec
, for example, defines a Stream
class for (monomorphic) streams of tokens to be used as input for a parser.
The Question
Making your question more concrete, let's ask:
Why isn't the type class
class HasHeadAndTail t where
head :: t a -> Maybe a
tail :: t a -> Maybe (t a)
isEmpty :: t a -> Bool
in the base
library?
An Answer
This class is only useful for ordered, linear containers. Map
, Set
, HashMap
, HashTable
, and Tree
all would not be instances. I'd even argue against making Seq
and DList
an instance since there are really two possible "heads" of that structure.
Also what can we say about any type that is an instance of this class? I think the only property is if isEmpty
is False then head
and tail
should be non-Nothing
. As a result, isEmpty shouldn't even be in the class and instead be a function isEmpty :: HashHeadAndTail t => t a -> Bool ; isEmpty = isNothing . head
.
So my answer is:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With