Description
There has been quite a bit of discussion about this going on for a long time, in #168, #370, on Gitter, and at the Typelevel Summit in Philly. But while #168 and #370 are highly relevant, there hasn't been an issue tracking this specifically.
At the Typelevel Summit in Philly, people seemed to come to a consensus that it would be good to not have type classes automatically provide default implementations of methods. For example, someone might want to define their own Monad
instance without inheriting a default implementation of map
in terms of flatMap
and pure
(that is likely less efficient than the version of map
that they would otherwise define) .
#368 is one approach to solving this problem. At the TL Summit, people seemed to be leaning toward a "minimum viable product" approach that is (arguably) a bit simpler. A type class trait
(Monad
for example) would contain only abstract def
signatures. Someone could extend Monad
without inheriting any default implementations. Each type class trait
would have a corresponding trait
that included default implementations (for example map
in terms of flatMap
and pure
on the trait corresponding to monad). So far all of the name suggestions that we have come up with for these traits with default implementations haven't seemed quite right. DerivedMonad
seems misleading, because it could be interpreted as a Monad
that is completely derived in a kittens-like fashion. MonadImpl
sounds a bit silly, but I guess at least it's concise.
A major motivator for this is that currently it's really easy to not realize that you are inheriting a default implementation whose performance is really bad. I came to want this when I started toying with adding an IsoFunctor
(basically a natural transformation in both directions) and I wanted to provide helpers that could create type class instances for your structure as long as you provided an IsoFunctor
between your structure and one that has the relevant type class instance. For example, if your JsonDecoder[A]
is isomorphic to Kleisli[Xor[Err, ?], Json, A]
, then you could define an IsoFunctor[JsonDecoder, Kleisli[Xor[Err, ?], Json, ?]
and get a derived Monad
instance that delegates through to the Monad
instance for Kleisli
. Defining these instances is very boiler-platey because you have to define each Monad
method and have it delegate through to the same method in the instance of the isomorphic type. It would be really easy for a new method to be added to Monad
without being added to the isoFunctorMonad
, meaning you would pick up the default Monad
implementation of that method and not benefit from any performance optimizations that had been added to the type class instance you are delegating through to. Removing the default implementations from the Monad
trait would mean you would be forced to resolve a compile error if you forgot to add the new method.
This approach is going to add a fair amount of boilerplate, but that boilerplate will only exist within cats and other code bases that want to go out of their way to make sure they aren't default implementations with poor performance. I think it's worth it. Are people still on board with going forward with this?