HORIZON HASKELLDocslts/ghc-9.10.x248f8f02026-10-05Search names, modules, packages, or :: a typeCtrl K

GHC 9.10.3 · lts/ghc-9.10.x · 248f8f0 · 2026-10-05

Moduledhall-1.42.3Haskell2010

Dhall.Deriving

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#derivingVia to jump there)

  • 16 types
  • 3 classes
  • 4 values
  • Packagedhall-1.42.3
  • Exports23
  • LanguageHaskell2010
  • LicenceBSD-3-Clause
  • SourceDeriving.hs

Introduction

0 declarations

Let's take the following Haskell data types:

Example1 expression
:set -XDerivingStrategies
Example1 expression
:{newtype Name = Name { getName :: Text }  deriving stock (Show):}
Example1 expression
:{data Font = Arial | ComicSans | Helvetica | TimesNewRoman  deriving stock (Show):}
Example1 expression
:{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

Example1 expression
:{font :: Decoder Fontfont =  union    (  (Arial         <$ constructor "Arial"           unit)    <> (ComicSans     <$ constructor "Comic Sans"      unit)    <> (Helvetica     <$ constructor "Helvetica"       unit)    <> (TimesNewRoman <$ constructor "Times New Roman" unit)    ):}
Example1 expression
:{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 Decoder auto 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.

Example2 expressions
:set -XStandaloneDeriving:set -XDeriveGeneric
Example1 expression
:{deriving stock instance Generic Namederiving stock instance Generic Fontderiving stock instance Generic Person:}
Example1 expression
:{instance FromDhall Nameinstance FromDhall Fontinstance FromDhall Person:}

However, when you try to read the same file with auto, you get this:

Example1 expression
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:

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:

Example4 expressions
:set -XDataKinds:set -XDeriveGeneric:set -XDerivingVia:set -XTypeOperators
Example1 expression
:{newtype Name = Name { getName :: Text }  deriving stock (Generic, Show)  deriving (FromDhall, ToDhall)    via Codec (SetSingletonConstructors Bare) Name:}
Example1 expression
:{data Font = Arial | ComicSans | Helvetica | TimesNewRoman  deriving stock (Generic, Show)  deriving (FromDhall, ToDhall)    via Codec (Constructor TitleCase) Font:}
Example1 expression
:{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:

Example2 expressions
simon <- input auto "./simon.dhall":: IO Personprint simonPerson {personName = Name {getName = "Simon"}, personFavoriteFont = ComicSans}

And using inject we can get simon back as a Dhall value:

Example3 expressions
import qualified Data.Text.IO as Textimport 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

0 declarations

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.

Field a and Constructor a can be used to modify, respectively, the fieldModifier and constructorModifier options of InterpretOptions, by post-composing the modifier with textFunction @a, that is, the value-level equivalent of a, 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, SetSingletonConstructors a can be used to set the singletonConstructors option of InterpretOptions, by replacing the option with the value-level equivalent of a.

DerivingVia newtype

1 declaration
newtypenewtype Codec (tag :: k) a
#

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.

Constructors

Instances2FromDhall, ToDhall

Type-level functions on InterpretOptions

4 declarations
classclass ModifyOptions (a :: k) where
#

Convert a type into a InterpretOptions -> InterpretOptions function

Instances5ModifyOptions
datadata Field (a :: k)
#

Field t post-composes the fieldModifier from options with the value-level version of t, obtained with TextFunction

Instances1ModifyOptions
datadata Constructor (a :: k)
#

Constructor t post-composes the constructorModifier from options with the value-level version of t, obtained with TextFunction

Instances1ModifyOptions

Type-level functions on Text

8 declarations
classclass TextFunction (a :: k) where
#

Convert a type into a Text -> Text function

Methods

Instances9TextFunction, …

Type-level versions of SingletonConstructors

4 declarations
typetype Bare = 'Bare
#

Type-level version of Bare. Never wrap the field of a singleton constructor in a record

typetype Wrapped = 'Wrapped
#

Type-level version of Wrapped Always wrap the field of a singleton constructor in a record

typetype Smart = 'Smart
#

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

2 declarations
typetype AsIs = ()
#

The identity for functions on InterpretOptions and on Text. Useful for deriving FromDhall and ToDhall with the default options.

Helper function on Text

1 declaration
valuedropPrefix :: Text -> Text -> Text
#

dropPrefix prefix text returns the suffix of text if its prefix matches prefix, or the entire text otherwise

InterpretOptions setters

3 declarations