Always automatically specialize some specific typeclasses

In haskell it’s very common pattern to use mtl-style typeclasses with monads, where in most of the code, you don’t write it for a concrete monad, but rather a list of the capabilities you want.

doSomething :: (MonadReader Cfg m, MonadError Err m) => Foo -> m Bar

which is very nice for both flexibility and the ability to show exactly what your code can do. However, it can sometimes have a large performance penalty [1] [2].

Now there are options like the {-# SPECIALIZE #-} pragma, but that would force you to litter every single function with it. And conversely, there’s -fspecialize-agressively, which solves the issue, but instead causes the compilation time and memory use of GHC to increase drastically [3].

What I’m suggesting is something in-between of these two options, where you make a global setting for your whole program to always specialize one list of typeclasses to another list of concrete types. One option would be that you can place these annotations either next to the declarations of the type classes or next to the concrete type. In other words, the same places where you can put an instance declaration.

The issue with the latter location is that it might be difficult for ghc to see that annotation in code that only imports the type class and not the concrete type, while an issue with the former option is that you can’t place the annotation on typeclasses which you don’t own.

Another option would be to not be explicit about the pair to specialize, but to say “always specialize this specific typeclass as much as possible” and/or “always specialize to this concrete type whenever you can”. This would for example allow us to place these on the generic monad type classes, since you’ll almost always want those specialized for performance. The risk here is that we don’t get much improvement over -fspecialize-agressively.

What do people think about this idea? Can something like this be done already? Is there a better place than here to bring this up?

1 Like

All I know is I would love if this was called -fspecialize-politely; i.e. it specialises only where you have politely indicated :slight_smile:

2 Likes

You missed the most important reference:

One of the main points is that full specialization duplicates the code of all polymorphic functions for every possible type you use those functions on. Thus you either have to limit yourself to one or at most a few concrete monads to avoid quadratic compilation time and space.

Fully specializing only a few classes will only help if there are many functions that are also polymorphic, but not in those specific classes. I don’t think that is actually the case for many libraries that use the mtl approach.

Instead, Alexis has written a library eff which uses a concrete Monad and thus requires no specialization at all. Since that talk, more libraries have appeared which take that approach, e.g. effectful. I’d recommend switching to one of those libraries if you run into this problem.

2 Likes

One way of programming against mtl-style typeclasses while still having specialization as good as with concrete types is to use Backpack.

(The usual objections and ergonomic concerns about Backpack apply.)

An example here. The idea is to define an abstract signature like

data M :: Type -> Type
instance Functor M
instance Applicative M
instance Monad M
instance MonadReader Int M
instance MonadState Int M

Of which we demand instances for all the classes that we want to use. The we can give concrete implementations to M.

One annoying thing about this technique is that, if you want to be completely precise about constraints, you need a different abstract monad signature for each different combination of MTL typeclasses your functions use.

1 Like

I like your idea. I imagine the pragma might look something like

{-# SPECIALIZE_CONSTRAINTS
  (MonadReader Cfg m, MonadError Err m) => (m ~ MyAppMonad) #-}

or even simply

{-# SPECIALIZE_CONSTRAINTS
  (MonadReader Cfg MyAppMonad, MonadError Err MyAppMonad) #-}

GHC would specialize every function whose class context is a generalization of the one specified by the pragma.

3 Likes