Safe Haskell | Safe-Inferred |
---|---|
Language | Haskell2010 |
- 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
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 Person
Person {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 simon
Person {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 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
Instances
ModifyOptions AsIs Source # | |
Defined in Dhall.Deriving | |
TextFunction a => ModifyOptions (Constructor a :: Type) Source # | |
Defined in Dhall.Deriving | |
TextFunction a => ModifyOptions (Field a :: Type) Source # | |
Defined in Dhall.Deriving | |
ToSingletonConstructors a => ModifyOptions (SetSingletonConstructors a :: Type) Source # | |
Defined in Dhall.Deriving | |
(ModifyOptions a, ModifyOptions b) => ModifyOptions (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving |
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 |
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 |
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 |
Type-level functions on Text
class TextFunction a where Source #
Convert a type into a Text -> Text
function
textFunction :: Text -> Text Source #
Instances
TextFunction AsIs Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction CamelCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction PascalCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction SnakeCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction SpinalCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction TitleCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
TextFunction TrainCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
KnownSymbol s => TextFunction (DropPrefix s :: Type) Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # | |
(TextFunction a, TextFunction b) => TextFunction (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving 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 textFunction :: Text -> Text Source # |
Convert casing to Title Cased Phrase
Instances
TextFunction TitleCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # |
Convert casing to camelCasedPhrase
Instances
TextFunction CamelCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # |
data PascalCase Source #
Convert casing to PascalCasedPhrase
Instances
TextFunction PascalCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # |
Convert casing to snake_cased_phrase
Instances
TextFunction SnakeCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # |
data SpinalCase Source #
Convert casing to spinal-cased-phrase
Instances
TextFunction SpinalCase Source # | |
Defined in Dhall.Deriving textFunction :: Text -> Text Source # |
Convert casing to Train-Cased-Phrase
Instances
TextFunction TrainCase Source # | |
Defined in Dhall.Deriving 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
Instances
ToSingletonConstructors Bare Source # | |
Defined in Dhall.Deriving | |
ToSingletonConstructors Smart Source # | |
Defined in Dhall.Deriving | |
ToSingletonConstructors Wrapped Source # | |
Defined in Dhall.Deriving |
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 | |
(TextFunction a, TextFunction b) => TextFunction (a <<< b :: Type) Source # | |
Defined in Dhall.Deriving 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
.