With all of the talk I've had about breaking changes in my libraries,
I definitely didn't want the Yesod world to feel left out. We've been
stable at yesod-core version 1.4 since 2014. But the changes going
through my package ecosystem towards MonadUnliftIO
are going to
affect Yesod as well. The question is: how significantly?
For those not aware, MonadUnliftIO
is an alternative typeclass to
both MonadBaseControl
and the MonadCatch
/MonadMask
classes in
monad-control
and exceptions
, respectively. I've mentioned the
advantages of this new approach in a number of places, but the best
resource is probably the
release announcement blog post.
At the simplest level, the breaking change in Yesod would consist of:
- Modifying
WidgetT
's internal representation. This is necessary since, currently, it's implemented as aWriterT
. Instead, to match withMonadUnliftIO
, it needs to be aReaderT
holding anIORef
. This is just about as minor a breaking change as I can imagine, since it only affects internal modules. (Said another way: it could even be argued to be a non-breaking change.) - Drop the
MonadBaseControl
andMonadCatch
/MonadMask
instances. This isn't strictly necessary, but has two advantages: it allows reduces the dependency footprint, and further encourages avoiding dangerous behavior, like usingconcurrently
with aStateT
on top ofHandlerT
. - Switch over to the new versions of the dependent libraries that are changing, in particular conduit and resourcet. (That's not technically a breaking change, but I typically consider dropping support for a major version of a dependency a semi-breaking change.)
- A number of minor cleanups that have been waiting for a breaking
changes. This includes things like adding strictness annotations in
a few places, and removing the defunct
GoogleEmail
andBrowserId
modules.
This is a perfectly reasonable set of changes to make, and we can easily call this Yesod 1.5 (or 2.0) and ship it. I'm going to share one more slightly larger change I've experimented with, and I'd appreciated feedback on whether it's worth the breakage to users of Yesod.
Away with transformers!
NOTE All comments here, as is usually the case in these discussions,
refer to code that must be in IO
anyway. Pure code gets a pass.
You can check out the changes (which appear larger than they actually
are) in
the no-transformers
branch. You'll
see shortly that that's a lie, but it does accurately indicate
intent. If you look at the pattern of the blog posts and recommended
best practices I've been discussing for the past year, it ultimately
comes down to a simple claim: we massively overuse monad transformers
in modern Haskell.
The most extreme response to this claim is that we should get rid of
all transformers, and just have our code live in IO
. I've made a
slight compromise to this for ergonomics, and decided it's worth
keeping reader capabilities, because it's a major pain (or at least
perceived major pain) to pass extra stuff around for, e.g., simple
functions like logInfo
.
The core data type for Yesod is HandlerT
, with code that looks like
getHomeR :: HandlerT App IO Html
. Under the surface, HandlerT
looks something like:
newtype HandlerT site m a = HandlerT (HandlerData site -> m a)
Let's ask a simple question: do we really need HandlerT
to be a
transformer? Why not simply rewrite it to be:
newtype HandlerFor site a = HandlerFor (HandlerData site -> IO a)
All we've done is replaced the m
type parameter with a concrete
selection of IO
. There are already assumptions all over the place
that your handlers will necessarily have IO
as the base monad, so
we're not really losing any generality. But what we gain is:
- Slightly clearer error messages
- Less type constraints, such as
MonadUnliftIO m
, floating around - Internally, this actually simplifies quite a few ugly things around weird type families
We can also regain a lot of backwards compatibility with a helper type synonym:
type HandlerT site m = HandlerFor site
Plus, if you're using the Handler
type synonym generated by the
Template Haskell code, the new version of Yesod would just generate
the right thing. Overall, this is a slight improvement, and we need to
weigh the benefit of it versus the cost of breakage. But let me throw
one other thing into the mix.
Handling subsite (yes, transformers)
I lied, twice: the new branch does use transformers, and HandlerT
is more general than HandlerFor
. In both cases, this has to do
with subsites, which have historically been a real pain to write
(using them hasn't been too bad). In fact, the entire reason we have
HandlerT
today is to try and make subsites work in a nicely layered
way (which I think I failed at). Those who have been using Yesod long
enough likely remember GHandler
as a previous approach for this. And
anyone who has played with writing a subsite, and the hell which
ensues when trying to use defaultLayout
, will agree that the
situation today is not great.
So cutting through all of the crap: when writing a subsite, almost everything is the same as writing normal handler code. The following differences pop up:
- When you call
getYesod
, you get the master site's app data (e.g.App
in a scaffolded site). You need some way to get the subsite's data as well (e.g., theStatic
value inyesod-static
). - When you call
getCurrentRoute
, it will give you a route in the master site. If you're insideyesod-auth
, for instance, you don't want to deal with all of the possible routes in the parent, but instead get a route for the subsite itself. - If I'm generated URLs, I need some way to convert the routes for a subsite into the parent site.
In today's Yesod, we provide these differences inside the HandlerT
type itself. This ends up adding some weird complications around
special-casing the base (and common) case where m
is IO
. Instead,
in the new branch, we have just one layer of ReaderT
sitting on top
of HandlerFor
, providing these three pieces of functionality. And if
you want to get a better view of this,
check out the code.
What to do?
Overall, I think this design is more elegant, easier to understand, and simplifies the codebase. In reality, I don't think it's either a major departure from the past, or a major improvement, which is what leaves me on the fence about the no transformer changes.
We're almost certainly going to have a breaking change in Yesod in the near future, but it need not include this change. If it doesn't, the breaking change will be the very minor one mentioned above. If the general consensus is in favor of this change, then we may as well throw it in at the same time.