Skip to content

Separate default implementations from type class definitions #992

Open
@ceedubs

Description

@ceedubs

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?

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions