In 2017, I wrote about writing an INI library in PureScript (and Haskell) here. Since that post, one major feature that impacts this library was released in v0.12.0: Instance Chain groups (see purescript/purescript#2315).
In the original article, we needed to convert parts of an INI document which was represented by this structure:
top level document
[section header 1]
field_name_a=field_value_a
field_name_b=field_value_b
[section header 2]
field_name_a=field_value_a
field_name_b=field_value_b
With this structure, we only really needed one generalized operation to be able to read multiple children from a parent, i.e. document -> sections, section -> fields. However, there was no clean way to apply the same application, and we had two classes to accomplish this:
class ReadDocumentSections (xs :: RowList) (from :: # Type) (to :: # Type)
| xs -> from to where
readDocumentSections ::
RLProxy xs
-> StrMap (StrMap String)
-> Except UhOhSpaghettios (Builder (Record from) (Record to))
class ReadSection (xs :: RowList) (from :: # Type) (to :: # Type)
| xs -> from to where
readSection ::
RLProxy xs
-> StrMap String
-> Except UhOhSpaghettios (Builder (Record from) (Record to))
While most everything looks to be the same, there is one critical difference we need to deal with: documents are represented by StrMap (StrMap String)
, while sections are represented as StrMap String
. Previously, we handled this by having two separate type classes, but this is overall unnecessary duplication. We can merge the two classes by putting the input type in the instance head:
class ReadLevel
(xs :: RowList)
(from :: # Type) (to :: # Type)
strmap
| xs strmap -> from to
where
readLevel :: RLProxy xs -> strmap -> Except UhOhSpaghettios (Builder { | from } { | to })
Then we can first define the base case where we close up the record:
instance nilReadLevel :: ReadLevel Nil () () strmap where
readLevel _ _ = pure identity
The section level can now be written as Object String
(Object
== StrMap
in 0.12) in the instance head:
instance consReadLevelSection ::
( IsSymbol name
, Row.Cons name ty from' to
, Row.Lacks name from'
, ReadIniField ty
, ReadLevel tail from from' (Object String)
) => ReadLevel (Cons name ty tail) from to (Object String) where
Then, while we could write the instance for document as the concretely typed nested Object
, we really could make life easier by writing a polymorphic instance. However, as forall a. Object a
overlaps with Object String
, we need to ensure that the Object String
instance comes first and apply instance chain groups. What all this means in the end is that else
goes on the end.
else instance consReadLevelDocument ::
( IsSymbol name
, Row.Cons name (Record inner) from' to
, Row.Lacks name from'
, RowToList inner xs
, ReadLevel xs () inner a
, ReadLevel tail from from' (Object a)
) => ReadLevel (Cons name (Record inner) tail) from to (Object a) where
And that's it!
Hopefully this has shown you one example of how some type class code that might previously have required duplication or overly specific instance heads can be simplified with instance chains.