| Safe Haskell | Safe-Inferred |
|---|---|
| Language | Haskell2010 |
Dhall.Deriving
Contents
- Introduction
- Writing FromDhall instances by hand
- Letting DerivingVia do the work
- Behind the scenes of Codec
- DerivingVia newtype
- Type-level functions on InterpretOptions
- Type-level functions on Text
- Type-level versions of SingletonConstructors
- Identity and Composition for ModifyOptions and TextFunction
- Helper function on Text
- InterpretOptions setters
Description
Newtypes for writing customizable FromDhall and ToDhall instances
through the DerivingVia strategy.
Inspired by Matt Parson's blog post Mirror Mirror: Reflection and Encoding Via, but applied to Dhall instead of JSON.
This module is intended to be used with DerivingVia so it's only available for GHC >= v8.6.1.
Check the section Letting DerivingVia do the work if you want to see this module in action. (Click Dhall.Deriving to jump there)
Synopsis
- newtype Codec tag a = Codec {
- unCodec :: a
- class ModifyOptions a where
- data Field a
- data Constructor a
- data SetSingletonConstructors a
- class TextFunction a where
- textFunction :: Text -> Text
- data DropPrefix (s :: Symbol)
- data TitleCase
- data CamelCase
- data PascalCase
- data SnakeCase
- data SpinalCase
- data TrainCase
- class ToSingletonConstructors (a :: SingletonConstructors) where
- type Bare = 'Bare
- type Wrapped = 'Wrapped
- type Smart = 'Smart
- type AsIs = ()
- data a <<< b
- dropPrefix :: Text -> Text -> Text
- addFieldModifier :: (Text -> Text) -> InterpretOptions -> InterpretOptions
- addConstructorModifier :: (Text -> Text) -> InterpretOptions -> InterpretOptions
- setSingletonConstructors :: SingletonConstructors -> InterpretOptions -> InterpretOptions
Introduction
Let's take the following Haskell data types:
>>>:set -XDerivingStrategies
>>>:{newtype Name = Name { getName :: Text } deriving stock (Show) :}
>>>:{data Font = Arial | ComicSans | Helvetica | TimesNewRoman deriving stock (Show) :}
>>>:{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
>>>:{font :: Decoder Font font = union ( (Arial <$ constructor "Arial" unit) <> (ComicSans <$ constructor "Comic Sans" unit) <> (Helvetica <$ constructor "Helvetica" unit) <> (TimesNewRoman <$ constructor "Times New Roman" unit) ) :}
>>>:{name :: Decoder Name name = Name <$> strictText :}
>>>:{person :: Decoder Person person = record ( Person <$> field "name" name <*> field "favoriteFont" font ) :}
and then you use it like this
>>>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 Decoder
auto instead of explicitly calling person.
Writing FromDhall instances by hand
"Aha!," you think, "I'll write an empty instance ".
That in turn requires you to add two other instances for FromDhall PersonFont and for Name,
plus Generic instances for each of those, but that's okay.
>>>:set -XStandaloneDeriving>>>:set -XDeriveGeneric
>>>:{deriving stock instance Generic Name deriving stock instance Generic Font deriving stock instance Generic Person :}
>>>:{instance FromDhall Name instance FromDhall Font instance FromDhall Person :}
However, when you try to read the same file with auto, you get this:
>>>input auto "./simon.dhall" :: IO Person*** Exception: ...Error...: Expression doesn't match annotation ... { - personFavoriteFont : … , - personName : … , + favoriteFont : … , + name : … } ... 1│ ./simon.dhall : { personName : { getName : Text } 2│ , personFavoriteFont : < Arial | ComicSans | Helvetica | TimesNewRoman > 3│ } ...
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:
>>>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:
>>>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:
>>>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:
>>>:{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.
>>>:{instance FromDhall Person where autoWith _ = genericAutoWith defaultInterpretOptions { fieldModifier = toCamel . Text.drop (Text.length "person") } :}
Now, for the moment of truth:
>>>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
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 ,
using a different Codec tag TheTypetag depending on the transformations we need to apply to
the Haskell type to get the Dhall equivalent:
>>>:set -XDataKinds>>>:set -XDeriveGeneric>>>:set -XDerivingVia>>>:set -XTypeOperators
>>>:{newtype Name = Name { getName :: Text } deriving stock (Generic, Show) deriving (FromDhall, ToDhall) via Codec (SetSingletonConstructors Bare) Name :}
>>>:{data Font = Arial | ComicSans | Helvetica | TimesNewRoman deriving stock (Generic, Show) deriving (FromDhall, ToDhall) via Codec (Constructor TitleCase) Font :}
>>>:{data Person = Person { personName :: Name , personFavoriteFont :: Font } deriving stock (Generic, Show) deriving (FromDhall, ToDhall) via Codec (Field (CamelCase <<< DropPrefix "person")) Person :}
we can then read the file using auto:
>>>simon <- input auto "./simon.dhall":: IO Person>>>print simonPerson {personName = Name {getName = "Simon"}, personFavoriteFont = ComicSans}
And using inject we can get simon back as a Dhall value:
>>>import qualified Data.Text.IO as Text>>>import Dhall.Core (pretty)>>>Text.putStrLn . pretty . embed inject $ simon{ name = "Simon" , favoriteFont = < Arial | `Comic Sans` | Helvetica | `Times New Roman` >.`Comic Sans` }
Behind the scenes of Codec
is really just a newtype over Codec tag aa, 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.
and Field a can be used to modify, respectively, the
Constructor afieldModifier and constructorModifier options of InterpretOptions, by
post-composing the modifier with , that is, the value-level
equivalent of textFunction @aa, obtained through the TextFunction class.
In the case of Person, we used
Codec (Field (CamelCase <<< DropPrefix "person")) Person
which means that the Text -> Text version of
CamelCase <<< DropPrefix "person"
was used to modify the fieldModifier option.
In the value level, this translates to composing (<<<)
toCamel (CamelCase) with
(dropPrefix "person").DropPrefix "person"
Finally, can be used to set the
SetSingletonConstructors asingletonConstructors option of InterpretOptions, by replacing the option
with the value-level equivalent of a.
DerivingVia newtype
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.
Instances
| (Generic a, GenericFromDhall a (Rep a), ModifyOptions tag) => FromDhall (Codec tag a) Source # | |
Defined in Dhall.Deriving | |
| (Generic a, GenericToDhall (Rep a), ModifyOptions tag) => ToDhall (Codec tag a) Source # | |
Defined in Dhall.Deriving Methods injectWith :: InputNormalizer -> Encoder (Codec tag a) Source # | |
Type-level functions on InterpretOptions
class ModifyOptions a where Source #
Convert a type into a InterpretOptions -> InterpretOptions function
Methods
modifyOptions :: InterpretOptions -> InterpretOptions Source #
Instances
| ModifyOptions AsIs Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
| TextFunction a => ModifyOptions (Constructor a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
| TextFunction a => ModifyOptions (Field a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
| ToSingletonConstructors a => ModifyOptions (SetSingletonConstructors a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
| (ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
Field t post-composes the fieldModifier from options with the
value-level version of t, obtained with TextFunction
Instances
| TextFunction a => ModifyOptions (Field a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
data Constructor a Source #
Constructor t post-composes the constructorModifier from options
with the value-level version of t, obtained with TextFunction
Instances
| TextFunction a => ModifyOptions (Constructor a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
data SetSingletonConstructors a Source #
SetSingletonConstructors t replaces the singletonConstructors
from options with the value-level version of t.
Instances
| ToSingletonConstructors a => ModifyOptions (SetSingletonConstructors a :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
Type-level functions on Text
class TextFunction a where Source #
Convert a type into a Text -> Text function
Methods
textFunction :: Text -> Text Source #
Instances
| TextFunction AsIs Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction CamelCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction PascalCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction SnakeCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction SpinalCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction TitleCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| TextFunction TrainCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| KnownSymbol s => TextFunction (DropPrefix s :: Type) Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
| (TextFunction a, TextFunction b) => TextFunction (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
data DropPrefix (s :: Symbol) Source #
DropPrefix prefix corresponds to the value level
function dropPrefix prefix
Instances
| KnownSymbol s => TextFunction (DropPrefix s :: Type) Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Convert casing to Title Cased Phrase
Instances
| TextFunction TitleCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Convert casing to camelCasedPhrase
Instances
| TextFunction CamelCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
data PascalCase Source #
Convert casing to PascalCasedPhrase
Instances
| TextFunction PascalCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Convert casing to snake_cased_phrase
Instances
| TextFunction SnakeCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
data SpinalCase Source #
Convert casing to spinal-cased-phrase
Instances
| TextFunction SpinalCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Convert casing to Train-Cased-Phrase
Instances
| TextFunction TrainCase Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Type-level versions of SingletonConstructors
class ToSingletonConstructors (a :: SingletonConstructors) where Source #
Convert a type of kind SingletonConstructors
into a value of type SingletonConstructors
Methods
Instances
| ToSingletonConstructors Bare Source # | |
Defined in Dhall.Deriving Methods | |
| ToSingletonConstructors Smart Source # | |
Defined in Dhall.Deriving Methods | |
| ToSingletonConstructors Wrapped Source # | |
Defined in Dhall.Deriving Methods | |
Type-level version of Bare.
Never wrap the field of a singleton constructor in a record
type Wrapped = 'Wrapped Source #
Type-level version of Wrapped
Always wrap the field of a singleton constructor in a record
Type-level version of Smart
Wrap the field of a singleton constructor in a record
only if the field is named
Identity and Composition for ModifyOptions and TextFunction
The identity for functions on InterpretOptions and on Text.
Useful for deriving FromDhall and ToDhall with the default options.
data a <<< b infixr 1 Source #
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)
Instances
| (ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving Methods modifyOptions :: InterpretOptions -> InterpretOptions Source # | |
| (TextFunction a, TextFunction b) => TextFunction (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving Methods textFunction :: Text -> Text Source # | |
Helper function on Text
dropPrefix :: Text -> Text -> Text Source #
dropPrefix prefix text returns the suffix of text if its prefix
matches prefix, or the entire text otherwise
InterpretOptions setters
addFieldModifier :: (Text -> Text) -> InterpretOptions -> InterpretOptions Source #
addFieldModifier f options post-composes the fieldModifier
from options with f.
addConstructorModifier :: (Text -> Text) -> InterpretOptions -> InterpretOptions Source #
addConstructorModifier f options post-composes the constructorModifier
from options with f.
setSingletonConstructors :: SingletonConstructors -> InterpretOptions -> InterpretOptions Source #
setSingletonConstructors v options replaces the singletonConstructors
from options with v.