loading...

Prefer Typeclasses over Records of Functions

louy2 profile image Yufan Lou ・2 min read

From Lessons learned while writing a Haskell application • gvolpe's blog

In the article above the author has promoted polymorphic record of functions to be a better alternative to typeclasses. I'd like to argue that, style differences notwithstanding, typeclasses are strictly better than records of functions.

Polymorphic record of functions is actually the fallback implementation strategy of typeclasses in GHC. (Alexis 35:58) Why is it the fallback? Because GHC optimizer cannot see the functions passed as closures within a record, and cannot optimize the use-site further. Using typeclasses at least sometimes enables optimization. Explicitly using records of functions prevents optimization.

Typeclasses are probably what come first when we think about polymorphic interfaces. However, they need laws / properties that define whether an instance is a valid one or not.

There are two angles we can look at this, one is the Haskell language, and one is the logic of the intention.

Typeclass as a Haskell language construct does not require any laws or properties. As it so happens from the research roots of Haskell, many typeclasses included in Haskell prelude are mathematical objects, which carry with them laws and properties necessary for composition. The laws and properties are not part of the Haskell language, and defining typeclasses without laws and properties is totally fine.

On the other hand, laws and properties are discovered from the use cases of the logic. So whether you represent the logic as a typeclass or as a record of functions, the laws and properties are still there as long as you are designing for the same use cases. The Prelude typeclasses are used for program composition, and their laws are discovered to be necessary for that use case. Whether they are passed in as a record or inlined, the laws do not change.

Secondly, we can only define a single instance for a specific type. This is called coherence. If we can do that, great! If we fail then we would be better off using a record of functions instead.

Coherence is what the compiler needs to uniquely identify which instance of the typeclass you are using. It is also what you need to do the same thing! If you can identify the unique instance but find that the compiler cannot, identify the differentiating factor you are using and add it to the typeclass as a parameter.

In the author's case, apparently two instances are identified based on using Redis or IORef Map, but this is not reflected in the Counter m, in which the parameter m is IO in both cases.

Reference:


  1. GHC sometimes specializes typeclass functions, that is, if it has the generic code available when it can derive which instances are needed, it will generate concrete functions for those instances. For this to be the case, though, either the generic code and the concrete usages have to be in the same module, which is usually not good code organization, or the generic code has to be marked INLINEABLE, which lengthens compile time and bloats the resulting binary.

Discussion

pic
Editor guide