>>> :{data Person = Person { personName :: Name , personFavoriteFont :: Font } deriving stock (Show):}
And assume we want to read the following Dhall file as a Person:
-- ./simon.dhall
let Name = Text
let Font = < Arial | `Comic Sans` | Helvetica | `Times New Roman` >
let Person = { name : Name, favoriteFont : Font }
in { name = "Simon", favoriteFont = Font.`Comic Sans` } : Person
Usually, you would build a Decoder by hand, like this
>>> :{name :: Decoder Namename = Name <$> strictText:}
Example1 expression
>>> :{person :: Decoder Personperson = record ( Person <$> field "name" name <*> field "favoriteFont" font ):}
and then you use it like this
Example1 expression
>>> input person "./simon.dhall"Person {personName = Name {getName = "Simon"}, personFavoriteFont = ComicSans}
So, it works! However, this is quite mechanic, and the compiler has pretty
much all the information it needs to do it for you. Besides, you'd like to
provide an instance of FromDhall so you can use the polymorphic Decoderauto instead of explicitly calling person.
Writing FromDhall instances by hand
0 declarations
"Aha!," you think, "I'll write an empty instance FromDhall Person".
That in turn requires you to add two other instances for Font and for Name,
plus Generic instances for each of those, but that's okay.
What happened? The field names don't quite match, since we're using prefixed
field names in Haskell but no prefixes in Dhall. "Okay," you think,
"I can write a custom instance which builds on Generic thanks to
genericAutoWith, I only need to supply a function to drop the prefixes
and camelCase the rest". So, using toCamel:
Example3 expressions
>>> import Data.Text.Manipulate (toCamel)>>> import qualified Data.Text as Text>>> :{instance FromDhall Person where autoWith _ = genericAutoWith defaultInterpretOptions { fieldModifier = toCamel . Text.drop (Text.length "person") }:}
Let's try to read that again:
Example1 expression
>>> input auto "./simon.dhall":: IO Person*** Exception:...Error...: Expression doesn't match annotation...{ favoriteFont : < - ComicSans : … | - TimesNewRoman : … | + `Comic Sans` : … | + `Times New Roman` : … | … >, name : - { … : … } (a record type) + Text}...1│ ./simon.dhall : { name : { getName : Text }2│ , favoriteFont : < Arial | ComicSans | Helvetica | TimesNewRoman >3│ }...
Okay, we're almost there. We have two things to solve now.
First, the Font constructors are PascalCased in Haskell,
but Title Cased in Dhall. We can communicate this to our
FromDhall instance using toTitle:
Example2 expressions
>>> import Data.Text.Manipulate (toTitle)>>> :{instance FromDhall Font where autoWith _ = genericAutoWith defaultInterpretOptions { constructorModifier = toTitle }:}
Second, we defined the Name type in Haskell as a newtype over Text, with a
getName field for unwrapping. In Dhall, however, Name is a synonym of
Text, which is why input above was expecting a record.
The Bare option for singletonConstructors is a perfect fit here:
it translates Haskell singleton constructors into the Dhall version of the
nested type, without wrapping it into a record.
We can then tweak our FromDhall instance like this:
Example1 expression
>>> :{instance FromDhall Name where autoWith _ = genericAutoWith defaultInterpretOptions { singletonConstructors = Bare }:}
Since we're running this interactively, we also need to update the
instance for Person, but it's the same as before.
Example1 expression
>>> :{instance FromDhall Person where autoWith _ = genericAutoWith defaultInterpretOptions { fieldModifier = toCamel . Text.drop (Text.length "person") }:}
Now, for the moment of truth:
Example1 expression
>>> input auto "./simon.dhall":: IO PersonPerson {personName = Name {getName = "Simon"}, personFavoriteFont = ComicSans}
That took a bit more work than we wanted, though, and a lot of it was just
boilerplate for defining the instances through genericAutoWith, tweaking
a single parameter at a time. Even worse, if we also wanted to provide
ToDhall instances we would need to keep the options in sync between both
instances, since otherwise the values wouldn't be able to round-trip from
Dhall to Dhall through Haskell.
Letting DerivingVia do the work
0 declarations
Starting with this dhall file:
-- ./simon.dhall
let Name = Text
let Font = < Arial | `Comic Sans` | Helvetica | `Times New Roman` >
let Person = { name : Name, favoriteFont : Font }
in { name = "Simon", favoriteFont = Font.`Comic Sans` } : Person
We can define the equivalent Haskell types as follows. Note that we
derive the FromDhall and ToDhall instances via Codec tag TheType,
using a different tag depending on the transformations we need to apply to
the Haskell type to get the Dhall equivalent:
Codec tag a is really just a newtype over a, equipped with a
phantom tag. The FromDhall instance for Codec uses the generic
representation of a, together with the InterpretOptions defined by tag as
a series of modifications to be applied on defaultInterpretOptions.
For the default behavior, using AsIs (a synonym for ()) as the tag
leaves the interpret options alone, so it's equivalent to the empty instance
we first tried to use.
Intended for use on deriving via clauses for types with a
Generic instance. The tag argument is used to construct an
InterpretOptions value which is used as the first argument
to genericAutoWith.
Composition for functions on InterpretOptions and on Text.
We use <<< since . isn't a valid type operator yet
(it will be valid starting from ghc-8.8.1)