Common Dhall Tasks for Config Files
Common Dhall Tasks for Config Files #
This page describes common tasks in using Dhall for generating config files. It provides the step-by-step for implementing these common features.
Enums (Sum Types) #
-
Define the type. Angle brackets around all the options, pipelines between the options. These are the names which appear in code, so it's alright if they follow your code convention rather than how they appear in the config file. So if you write enums in all capital letters because they're basically constants, you do you.
:/types/ServiceType.dhall
< simple | forking | oneshot | notify | dbus | idle >
-
Register it in the types.dhall file:
:/types.dhall
{... , ServiceType = ./types/ServiceType.dhall }
-
Use it in an example by referencing through the type import:
let ServiceType = Systemd.ServiceType.simple
-
Make a renderer for the type. Start with the import of the types files. Then we make a function which merges a record with the string equivalents with the input. The only thing which falls out is the correct string equivalent. Neat!
:/render/ServiceType.dhall
let types = ../types.dhall in λ(x : types.ServiceType) → merge { simple = "simple" , forking = "forking" , oneshot = "oneshot" , notify = "notify" , dbus = "dbus" , idle = "idle" } x
-
Register the renderer with:
:/render.dhall
{... , ServiceType = ./render/ServiceType.dhall }
-
Use it in other renderers like normal. Since we're in the 'render' folder, we can just import it with a local import, like
./ServiceType.dhall
:/render/Service.dhall
let types = ../types.dhall in λ(i : types.Service) → '' [Service] User=${i.User} ExecStart=${i.ExecStart} Type=${./ServiceType.dhall i.ServiceType} ''
Unions | Complex Sum Types #
Sometimes you've got a field which is a value of Type a or Type b. A sum type represents that. This example combines an enum and Text, but the same principle holds for any types. Here's a link for the official tutorial
-
Create the sum type similarly to how you'd create an Enum. Note that each option has both a name and a Type. These also generate the constructors for the Union type.
:/types/Dependency.dhall
< Runlevel : ./RunlevelTargets.dhall | Service : Text >
-
Register it in the ':/types.dhall'
-
Make a renderer for the type. You can use merge to select a function and then apply it. You can compose this from other functions you have already. Note that we use
=
here, since this isn't defining a type signature. Also note that we're usingid
,Text/show
was adding extra quotes.:/render/Dependency.dhall
let types = ../types.dhall let id = λ(a : Type) → λ(x : a) → x in λ(x : types.Dependency) → merge { Runlevel = ./RunlevelTargets.dhall, Service = id Text } x
-
Register it in the ':/render.dhall'
-
Use it by accessing the constructor you want. Note that the renderer is the same, since they're ultimately the same type:
let Systemd = ../types.dhall let Render = ./reder.dhall in '' ${Render.Dependency (Systemd.Dependency.Service "nfs-common.service")} ${Render.Dependency (Systemd.Dependency.Runlevel Systemd.Runlevel.multiuser)} ''
Optional fields (autoexclusion) #
Sometimes you want a field to not appear if there is no value defined.
We'll define a helper function to render optionals. I use it frequently so I gave it a short name. Mine adds a newline automatically, so they don't appear if the option doesn't render.
:/render/ro.dhall
{-
a: Type parameter
f: Renderer
opt: Value
-}
λ(a : Type) →
λ(f : a → Text) →
λ(opt : Optional a) →
merge { Some = λ(x : a) → f x ++ "\n", None = "" } opt
Using that one is a bit clunky:
in λ(i : types.Install) →
''
[Install]
''
++ ro
types.Dependency
(λ(l : types.Dependency) → "WantedBy=${./Dependency.dhall l}")
i.WantedBy
So I made this following helper, which works better with the config file format:
:/render/ron.dhall
let ro = ./ro.dhall
in λ(a : Type) →
λ(n : Text) →
λ(f : a → Text) →
λ(opt : Optional a) →
ro a (λ(l : a) → "${n}=${f l}") opt
And can be used like:
in λ(i : types.Install) →
''
[Install]
''
++ ron types.Dependency "WantedBy" ./Dependency.dhall i.WantedBy
If you find yourself frequently using the same type (for example, bools), you can make another wrapper for that. The dhall-nethack repo makes significant use of those, see the config class
Multiple exports per file #
Dhall likes you to have a single type per file. But sometimes you have a group of types which don't make sense separately. Or you have a lot of types which are basically just type aliases for primitive types. You can export them in a file like so
:/ex.dhall
{ Thing0 = Text, Thing1 = Integer }
and then use them like
let ex = ./ex.dhall
let y
: ex.Thing0
= "HELLO"
in y
If some of your types reference another, you'll need to pull the referenced type into a let
higher in the file:
:/ex.dhall
let T
: Type
= Text
in { T, ListOfT = List T }