Search code examples
functional-programmingpurescript

Compile type error when trying to return Extendable record


I have created an extendable record type and how can I return the minimal base type without the compiler throwing error.

I have a record with type,

type SectionContent r = 
   { titleText :: String 
   , subTitleText :: String
   , ....
   | r 
   }

I'm Extending it, like

type MyConfig = 
    { section1 : SectionContent ()
    , section2 : SectionContent (someContent :: {...})
    ..
    }

When I try to access in my code like this,

  -- This is inside another function & I have access to config
  currenScreenConfig :: forall r. SectionContent r 
  currenScreenConfig = if predicate 
                         then config.section1
                         else config.section2 

It's throwing error,

Could not match type
                                                 
    ( someContent :: {...})                                            
                                                 
  with type
      
    r0  
.....
where r0 is a rigid type variable
    bound at ...
  ....

And what exactly rigid type mean here?


Solution

  • This is a classic confusion about who gets to choose the generic parameters.

    When you access (or "reference", or "use") a value whose type has a forall r. in front, you get to choose what r is. And whoever implemented that value must make it such that it would work with the r you choose. Without knowing what you will choose in advance.

    In other words: it's the caller of the function that gets to choose type parameters, not the implementer.

    Therefore, somebody calling your currenScreenConfig function later might decide to choose r ~ ( foo :: String ). And then your function would have to return a record SectionContent ( foo :: String ).

    And another caller, in a different place, might choose ( bar :: Int ), and then your function would have to return a record SectionContent ( bar :: Int ).

    Do you see how there is no way to implement such function?


    Now, if you want to return just SectionContent (), then that's what the type of the function should be:

    currenScreenConfig :: SectionContent ()
    

    But of course with that you can't return config.section2, because it has a different type.


    There are ways to "trim" a record (i.e. throw away unneeded fields). One such way is the pick function from the record-extra package, which you could use like this:

    currenScreenConfig :: SectionContent ()
    currenScreenConfig = if predicate then config.section1 else pick config.section2
    

    However, I encourage you to employ a more natural model instead. If the "common" part of the various screen contents is commonly extracted like that, it would be more natural to include it as a field rather than merge:

    type SectionContentCommon =
      { titleText :: String 
      , subTitleText :: String
      , ....
      }
    
    type SectionContent r =
      { common :: SectionContentCommon
      | r
      }
    
    type MyConfig = ... same ... 
    
    currenScreenConfig :: SectionContent ()
    currenScreenConfig = if predicate then config.section1.common else config.section2.common
    

    It may seem cool and shiny to use extensible records, but I strongly encourage you to consider a more straightforward model instead. Trust me: cool and shiny never outweighs long-term maintenance.