Short answers:
The question’s key points, as I understand them, are the following:
- “is the auto-specialization transitive?”
- Should I only expect (+) to be specialized transitively with an explicit pragma?
- (apparently intended) Is this a bug of GHC? Is it inconsistent with the documentation?
AFAIK, the answers are No, mostly yes but there are other means, and No.
Code inlining and type application specialization is a trade-off between speed (execution time) and code size. The default level gets some speedup without bloating the code. Choosing a more exhaustive level is left to the programmer’s discretion via SPECIALISE pragma.
Explanation:
The optimiser also considers each imported INLINABLE overloaded function, and specialises it for the different types at which it is called in M.
Suppose f is a function whose type includes a type variable a constrained by a type class C a. GHC by default specializes f with respect to a type application (substituting a for t) if f is called with that type application in the source code of (a) any function in the same module, or (b) if f is marked INLINABLE, then any other module that imports f from B. Thus, auto-specialization is not transitive, it only touches INLINABLE functions imported and called for in the source code of A.
In your example, if you rewrite the instance of Num as follows:
instance (Num r, Unbox r) => Num (Qux r) where
(+) = quxAdd
quxAdd (Qux x) (Qux y) = Qux $ U.zipWith (+) x y
quxAddis not specifically imported byMain.Mainimports the instance dictionary ofNum (Qux Int), and this dictionary containsquxAddin the record for(+). However, although the dictionary is imported, the contents used in the dictionary are not.plusdoes not callquxAdd, it uses the function stored for the(+)record in the instance dictionary ofNum t. This dictionary is set at the call site (inMain) by the compiler.