Consider the following recursive data type:
data Chain a = End | Link a (Chain a)
By deriving a Generic
instance for this recursive type, show
can be defined in terms of genericShow
:
derive instance Generic (Chain a) _
instance Show a => Show (Chain a) where
show = genericShow
Note that this show
is eta reduced. Now, if I try to display a Chain Int
value:
logShow $ Link 1 $ Link 2 $ End
This code overflows the stack when executed. However, that is not the case for an eta-expanded definition of show
:
instance Show a => Show (Chain a) where
show c = genericShow c
The existing documentation mentions this issue, but I don't understand the reason behind it. I suspect this has to do with the way genericShow
is generated. Any ideas?
Thank you
At the level of compiled code (i.e. in JavaScript) class instances are represented as dictionaries. Or, in JavaScript parlance, objects. And these dictionaries/objects are passed around as parameters whenever you have a class constraint in your code.
So, for example, if you have a class and a function like:
class Foo a where foo :: a -> Int
f :: forall a. Foo a => a -> String
f x = show $ foo x
In JavaScript this looks like:
const f = (fooDictionary) => (x) => showInt.show(fooDictionary.foo(x))
Notice that fooDictionary
is expected to have a key named foo
- that corresponds to the method foo :: a -> Int
defined in the class.
Also notice that the showInt
instance wasn't passed as a parameter to f
, but was plucked out of ambient context. This is because the compiler already knows that it's Int
it's working with, so it knows which dictionary to pick. But function f
doesn't know which unknown type a
it's working with, so it has to take the Foo a
dictionary as a parameter.
When you define an instance Show (Chain a)
in η-expanded form, in JavaScript it looks something like this:
const showChainDictionary =
(showADictionary) =>
({ show: (x) => genericShow(genericChainDictionary, showADictionary)(x) })
The thing of note here is that your dictionary definition is itself a function. Why? Because it needs a Show a
dictionary in order to pass down to genericShow
so that it knows how to show the a
parameter. So when somebody wants to show Chain Int
, for example, they would go:
const showChainIntDictionary = showChainDictionary(showIntDictionary)
const result = showChainIntDictionary.show( ... )
See how this works?
Well, I oversimplified a bit. In reality, genericShow
's parameters are a whole buttload of nested calls to create a buttload of Generic
-related dictionaries. Something more like this:
const showChainDictionary =
(showADictionary) => {
return {
show: function (x) {
const gShow = genericShow(
genericShowSumDict(
genericShowConstructorDict("End"),
genericShowConstructorDict("Link",
genericShowArgDict(showADictionary),
genericShowArgDict(showChainDictionary(showADictionary))
)
)
)
return gShow(x)
}
}
This is also not the completely correct definition, but it shows the principle. See how genericShow
takes a dictionary that can show the whole Chain
type, which is a sum type, hence genericShowSumDict
. And that one in turn takes two dictionaries, one to show each of the constructors - first End
, then Link
. And this very last one also takes two dictionaries to show the arguments of the Link
constructor - one showADictionary
for the a
argument and second showChainDictionary(showADictionary)
for the Chain a
argument.
See how the very last call is recursive? The showChainDictionary
function calls itself in its body. But it's ok. Recursive calls are permitted.
Take that definition and try to η-reduce the show
function in its body:
const showChainDictionary =
(showADictionary) => {
return {
show: genericShow(
genericShowSumDict(
genericShowConstructorDict("End"),
genericShowConstructorDict("Link",
genericShowArgDict(showADictionary),
genericShowArgDict(showChainDictionary(showADictionary))
)
)
)
}
See what happened? As soon as the showChainDictionary
function is called, it immediately calls itself. Recursion is now immediate, not deferred to when the show
function itself is called.
And that's your stack overflow.