In Haskell to define an instance of a type class you need to supply a dictionary of functions required by the type class. I.e. to define an instance of Bounded, you need to supply a definition for minBound and maxBound.
For the purpose of this question, let’s call this dictionary the vtbl for the type class instance. Let me know if this is poor analogy.
My question centers around what kind of code generation can I expect from GHC when I call a type class function. In such cases I see three possibilities:
- the vtbl lookup to find the implementation function is down at run time
- the vtbl lookup is done at compile time and a direct call to the implementation function is emitted in the generated code
- the vtbl lookup is done at compile time and the implementation function is inlined at the call site
I’d like to understand when each of these occur – or if there are other possibilities.
Also, does it matter if the type class was defined in a separately compiled module as opposed to being part of the “main” compilation unit?
In a runnable program it seems that Haskell knows the types of all the functions and expressions in the program. Therefore, when I call a type class function the compiler should know what the vtbl is and exactly which implementation function to call. I would expect the compiler to at least generate a direct call to implementation function. Is this true?
(I say “runnable program” here to distinguish it from compiling a module which you don’t run.)
As with all good questions, the answer is “it depends”. The rule of thumb is that there’s a runtime cost to any typeclass-polymorphic code. However, library authors have a lot of flexibility in eliminating this cost with GHC’s rewrite rules, and in particular there is a
{-# SPECIALIZE #-}pragma that can automatically create monomorphic versions of polymorphic functions and use them whenever the polymorphic function can be inferred to be used at the monomorphic type. (The price for doing this is library and executable size, I think.)You can answer your question for any particular code segment using ghc’s
-ddump-simplflag. For example, here’s a short Haskell file:Without optimizations, you can see that GHC does the dictionary lookup at runtime:
…the relevant bit being
realToFrac @Int @Double. At-O2, on the other hand, you can see it did the dictionary lookup statically and inlined the implementation, the result being a single call toint2Double#:It’s also possible for a library author to choose to rewrite the polymorphic function to a call to a monomorphic one but not inline the implementation of the monomorphic one; this means that all of the possibilities you proposed (and more) are possible.