stack-overflowpurescriptrecursive-datastructuresderiving

Why eta-reduced of genericShow-based show definition overflows stack for recursive types


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


Solution

  • First: the primer on how instances work

    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.


    Now, next level: your particular instance

    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?


    Next step: address the lies.

    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.


    And now, the final step: η-reduction.

    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.