ClassyPrelude: The good, the bad, and the ugly
August 3, 2012
First a message from our sponsors: Yesod 1.1 is fully tested and ready for release. I'm holding off until the beginning of next week, since I hate making major releases on a Friday (if there are any bugs, I won't be around on Saturday to fix them). If you're really eager to check it out, you can get the latest and greatest from Github.
I promised to write this blog post a week or two ago, but never got around to it. Dan Burton reminded me about it yesterday... so here's paying off my debt :).
So far, most of the discussion around classy-prelude has focused around the "classy" bits. As Felipe mentioned a few weeks ago, there's a lot more going on than just the classy parts. So I'd like to break open the discussion more and explain what exactly is going on.
Before we start, I think I need to give a bit of an explanation of my development strategy, as I think some people don't understand my approach. When attacking a problem for the first time, I throw in the kitchen sink. Many people prefer to take a more iterative approach, adding features one step at a time after a lot of careful testing. And in many cases, I think that's the only valid approach.
But for a project like
classy-prelude, I'm interested in being much more
experimental. That means I'm happy to throw in some half-baked ideas to see if
they work. Don't mistake that as me saying I fully endorse and recommend this
approach. On the contrary, as I hope this post makes clear, I'm highly
skeptical of some the choices I made. But I'm also a firm believer that we
won't know if the choices were bad unless we try them out.
So without further ado, let's break down the
classy-prelude, starting with
the "obvious good" category and moving further into the unknown. (And I think
it goes without saying that statements like "obviously good" are subjective.)
Win: Full compatibility with standard libraries
I saw the contrary claim made many times in various discussions, so let's put
it to rest. Unlike other prelude replacements,
classy-prelude does not try
to fix flaws in the base package.
Functor is not a superclass of
fail still exists, and
Num is unchanged. That's not to say that I think
that the base package did everything correctly (I don't), but rather, by
sticking to the standard typeclasses,
classy-prelude code should work with
any existing library without a hitch.
From a user perspective: you should be able to use
classy-prelude in one
module, ditch it in another, depend on some packages that use it and others
that don't, without any problem. In fact, I've done exactly that in some of my
code. If you use
classy-prelude in your library, your library users should
not be affected beyond the fact that they have to install
As an aside, I'd like to see some of those bigger changes make it into base
ultimately, and I think an alternate prelude is a great way to play around with
the ideas. But such an alternate prelude wouldn't be much use for real-world
use, which is why I haven't taken that route with
Win: Don't export partial functions
I really wish
Prelude didn't export
tail, and a bunch of other
partial functions. So
classy-prelude doesn't. More generally, I've been
taking a whitelist approach to which
Prelude functions are exported. If I've
left out one of your favorite functions, it's likely because I just didn't get
around to it yet, not due to any hatred of it. If there's some total function
Prelude that you'd like added, just send a pull request.
Win: Export commonly used functions from other modules
I prefer using
fmap in most circumstances. I like to use
&&&, and a huge number of my modules import
mapMaybe. All of these
functions/operators have names that are well recognized in the community, I
believe are unambiguous, and are very generally useful. Since the main purpose
classy-prelude is to save you from extra keystrokes, let's just export
them all by default.
The selection here is clearly going to be pretty opinionated. And the list isn't really that exhaustive right now (again, pull requests welcome). But while we can argue over exactly which functions should be exported, I think this is a pretty solid advantage.
Win: Export datatypes
How many modules have you written with lines looking like:
import Data.ByteString (ByteString)
And how many times do you just import
Data.ByteString qualified and use
S.ByteString? How often is
ByteString imported from
instead? And do you make the alias for the module
These are all thoroughly uninteresting questions. Typing out the code is
boring. And trying to remember which convention is used in the particular
module you're working on is just a pain. So in
classy-prelude, we have a
simple convention: export the datatype. If there's a lazy variant, prefix it
L. So we end up with
HashMap, and so on.
There's nothing earth-shattering here. It just removes an annoyance.
Probably a win: Generalize using existing typeclasses
Monoid a lot. I think it's a great typeclass. And I think it's a shame
concat only works for lists and not arbitrary
Similarly, I wish that
++ was just
mappend. So in
done just that.
This is a bit controversial, because it can make error messages a bit more
confusing. But let me reinforce something that I probably didn't clarify
classy-prelude is not intended for beginners. I don't think that a
Haskell tutorial should be using it for examples.
classy-prelude is intended
for Haskellers who are already battle-tested with GHC's error messages, and
won't shy away from some more general types.
One change I didn't make, but was considering, was using
Control.Category. I'm not sure how much of a practical benefit that would
provide people, while making error messages likely much more complex. But if
people want that change, let's discuss it.
More debatable win: recommend better datatypes
I've gone on record many times saying that I dislike
type FilePath = [Char].
(yet another) wonderful package by John Millikin, and I use it extensively in my
personal code. (It's also making its way more solidly into the Yesod ecosystem,
though we frankly don't do that much filesystem access in Yesod.)
classy-prelude, I export
FilePath type. This is
likely the most "breaking change" we have, though it doesn't really prevent you
from continuing to use
[Char] for all your filepaths.
exports a bunch of the functions and operators for manipulating filepaths, like
Again, this is an opinionated decision. But I think moving over to
system-filepath could be a huge win for the Haskell community in general, and
classy-prelude's future, I hope it catches on.
More controversial: create a bunch of new typeclasses
Now we get to the fun stuff.
classy-prelude defines a bunch of new
typeclasses for functions which are commonly imported qualified. I mentioned it
previously, but I don't think my point got across, so I'll say it again. The
purpose here has nothing to do with equational reasoning. This is purely a
technique for name overloading, nothing more.
Let's expand on that a bit. One of the nice things about typeclasses is that
they are usually associated with a set of laws that define their behavior. This
lets us write code against a typeclass and know, based on the laws, how it will
behave. We don't need to know if we're dealing with a list of a
Set, we know
mappend empty foo is the same as
That's not my purpose here. The purpose is purely about name overloading. I
haven't stated a semantics for what
lookup needs to do, because the answer is
it depends on the container. This means that
classy-prelude is not
decreasing the cognitive load of the programmer in any meaningful way. If there
are semantic differences between the
insert functions for a
HashMap and a
Map, you still need to know about them. (To my knowledge, no such difference
So let's say it again: the only purpose for this technique is to allow name overloading.
Based on this, I think it's fair to claim that
classy-prelude doesn't make
code more buggy due to its lack of associated laws. The libraries you're using
already don't conform to a set of laws.
classy-prelude is just making it
easier to use them. If you write type-generic code and try to use it for
multiple container types (not a practice I recommend), it's possible that
you'll get buggy code due to semantic differences. But the same applies if you
write a function using
Data.Set, copy-paste it, and then replace
All that said, I'm not saying that a set of associated laws isn't a good thing.
I'd love to add them. And if
classy-prelude continues, I'm sure we will add
them. But for the initial proof-of-concept experimental release, I don't think
it was worth the investment of time.
One final idea here is to leverage some existing collections of typeclasses (Edward Kmett's reducers package in particular) instead of defining our own typeclasses. Again, for the proof-of-concept, that idea was premature, but going forward I'm hoping to look into it. There are obstacles we'd have to overcome (like ensuring good performance), but I don't think anything is insurmountable.
Very controversial: make those typeclasses work with
I was surprised not to see as much of a discussion about this point as the
previous one. In my mind, this was by far the most controversial choice in
classy-prelude. It's fair to say that I put it in more out of curiosity if it
would work than anything else.
filter as an example. A relatively simple typeclass to model it
would look like:
class CanFilter container element | container -> element where filter :: (element -> bool) -> container -> container
This would work for lists,
Set, and so on. However, here's the
(simplified) signature of
filter :: (i -> Bool) -> Conduit i m i
This doesn't fit in with our
CanFilter class above.
CanFilter has a
function that takes two arguments, whereas in
filter has just one
So here's the trick. We recognize that the first argument is the same (a
predicate), and just leverage currying. Recognizing that
can be viewed as a function of one argument returning a function, we split it
up into two typeclasses:
class CanFilter result element where filter :: (element -> bool) -> result class CanFilterFunc container element | container -> element where filterFunc :: (element -> bool) -> container -> container
And then we define a single instance of
CanFilter that uses
for all cases where we return a function:
instance (container ~ container', CanFilterFunc container element) => CanFilter (container -> container') element where filter = filterFunc
(If you're curious about the equality constraint, see this excellent Stack Overflow answer.)
We can then separately define an instance for
conduit, which does not overlap
at all, and allows us to reuse the name
filter in significantly different use
cases. Frankly, I think it's pretty cool that we have this kind of flexibility
in the type class system. But from a practical standpoint, I'm not sure it's
really a great trade-off:
Since the two
filterconcepts work fairly differently, it may be more confusing than anything else to lump them together.
Error messages get significantly more confusing.
I don't think that
conduitusage in this sense is prevalent enough to warrant the costs.
In other words, if there's something I'd want to cut out first from
classy-prelude, it's the gymnastics which it pulls to accomodate
instances. It was definitely a fun part of the experiment, and I'm glad to have
tested it. If anyone has an opinion either way on this, let me know.
There are a number of minor bugs in the typeclass definitions in
classy-prelude, requiring more explicit type signatures than should be
necessary in some cases. Those kinds of things are easily fixed. If people find
specific examples, please bring them to my attention.
Dan Burton started a project called
which takes a different approach to the namespace issue, replicating first
class modules via the record system. I think it's definitely an interesting
approach, and think it can coexist very well with
classy-prelude. But it's
not enough for my taste: I prefer the relative terseness of typeclass-based
However, putting that project together with my points in this post, I think
it's important to note that there is a very large part of
which has nothing to do with typeclasses. Dan called this
and I think it makes a lot of sense to provide that separately (though at least
for now, I'll keep it in the same package for convenience).
The idea is simple: I think a lot of people agree with me that some major
aspects of this prelude are no-brainers, and would like to use them. People are
understably more wary of the experimental bits. (As I've said before, so am I:
I won't allow
classy-prelude to be used in
yesod in its current state.) So
let's create a stable basis, and encourage experimentation. As I mentioned at
the beginning, since we have full compatibility with
base, there's no real
concern of fragmentation.
Once I remove the
conduit aspects of the library, I think it will open it up
for much better analysis of typeclass laws. It might be possible to completely
drop a number of typeclasses and use the
reducers versions instead. For
others, more experimentation might be necessary.
And as usual: feedback is welcome, especially the constructive kind :).