Dhall is a typed and programmable configuration language which you can:
... and now you can also use Dhall as a template engine with the newly released dhall-text
library which provides a dhall-to-text
executable for templating text.
This executable actually does not do very much: all the code does is check that the Dhall expression has type Text
and then renders the Text
. Most of the work to support template engine features actually consists of improvements to the core Dhall language. That means that all the features I'm highlighting in this post also benefit the other Dhall integrations.
You can learn more about Dhall by reading the official tutorial but I can also illustrate how dhall-to-text
works by comparing to Mustache, which is one of the more widely used template engines. All of the following examples come from the Mustache manual for the Ruby library.
Initial example
Mustache is a text templating engine that subdivides the work of templating into two parts:
- The text to template
- The data to template the text with
For example, given the following template:
Hello {{name}}
You have just won {{value}} dollars!
{{#in_ca}}
Well, {{taxed_value}} dollars, after taxes.
{{/in_ca}}
... and the following data:
{
"name": "Chris",
"value": 10000,
"taxed_value": 10000 - (10000 * 0.4),
"in_ca": true
}
... we get the following output when we combine the two:
Hello Chris
You have just won 10000 dollars!
Well, 6000.0 dollars, after taxes.
In Dhall, there is no distinction between the template and the data. They are both Dhall expressions. A template is just a Dhall function and the data is just an argument that we pass to that function.
For example, the above template translates to this Dhall file:
$ cat function
\(record : { name : Text
, value : Double
, taxed_value : Double
, in_ca : Bool
}
) -> ''
Hello ${record.name}
You have just won ${Double/show record.value} dollars!
${ if record.in_ca
then "Well, ${Double/show record.taxed_value} dollars, after taxes"
else ""
}
''
... and the above data payload translates to this Dhall file:
$ cat value
{ name = "Chris"
, value = 10000.0
, taxed_value = 6000.0
, in_ca = True
}
... and we can combine the two using the dhall-to-text
executable by applying the function to the argument:
$ dhall-to-text <<< './function ./value'
Hello Chris
You have just won 10000.0 dollars!
Well, 6000.0 dollars, after taxes
This example already highlights several features of Dhall which the next section will walk through
Dhall basics
Dhall is a functional programming language and supports anonymous functions of the form:
\(functionArgumentName : functionArgumentType) -> functionResult
For example, this template:
\(record : { name : Text
, value : Double
, taxed_value : Double
, in_ca : Bool
}
) -> ''
Hello ${record.name}
You have just won ${Double/show record.value} dollars!
${ if record.in_ca
then "Well, ${Double/show record.taxed_value} dollars, after taxes"
else ""
}
''
... is just one large function where:
the function argument name is
record
the function argument type is the following anonymous record type:
{ name : Text , value : Double , taxed_value : Double , in_ca : Bool }
the function result is a multiline string literal
'' Hello ${record.name} You have just won ${Double/show record.value} dollars! ${ if record.in_ca then "Well, ${Double/show record.taxed_value} dollars, after taxes" else "" } ''
Multiline string literals use the same syntax as The Nix language: two single quotes to open and close the string. Dhall also supports the ordinary string literals you know and love using double quotes, such as:
"Well, ${Double/show record.taxed_value} dollars, after taxes"
We can interpolate any Dhall expression of type Text
into a string literal using ${...}
syntax (another newly added Dhall feature). We cannot automatically interpolate other types of values like Double
s, so we have to explicitly convert them with a function like Double/show
.
Interpolation works for arbitrarily long Dhall expressions as long as they have type Text
. This is why we can interpolate an if
expression, like this:
''
...
${ if record.in_ca
then "Well, ${Double/show record.taxed_value} dollars, after taxes"
else ""
}
...
''
Dhall lets us import other Dhall expressions by their file path, URL, or even via environment variables. For example, we were already using this feature when evaluating our template:
$ dhall-to-text <<< './function ./value'
...
./function ./value
is yet another valid Dhall expression that replaces ./function
and ./value
with the corresponding expression stored within each respective file.
Types
Dhall is typed and will catch errors in our template files. If our record is missing any fields then that's a type error. For example:
$ dhall-to-text <<< './function { name = "Chris" }'
dhall-to-text:
Error: Wrong type of function argument
./example0 { name = "Chris" }
(input):1:1
We can also obtain more detailed information by adding the --explain
flag:
$ dhall-to-text --explain <<< './function { name = "Chris" }'
Error: Wrong type of function argument
Explanation: Every function declares what type or kind of argu...
For example:
┌───────────────────────────────┐
│ λ(x : Bool) → x : Bool → Bool │ This anonymous function...
└───────────────────────────────┘ arguments that have typ...
⇧
The function's input type
...
{ Lots of helpful explanation that I'm cutting out for brevity }
...
You tried to invoke the following function:
↳ λ(record : { in_ca : Bool, name : Text, taxed_value : Double...
... which expects an argument of type or kind:
↳ { in_ca : Bool, name : Text, taxed_value : Double, value : D...
... on the following argument:
↳ { name = "Chris" }
... which has a different type or kind:
↳ { name : Text }
──────────────────────────────────────────────────────────────...
./example0 { name = "Chris" }
(stdin):1:1
These type safety guarantees protect us against unintentional templating errors.
Dhall does support optional fields and values, though, but you have to explicitly opt into them because all values are required by default. The next section covers how to produce and consume optional values.
Optional fields
In Mustache, if we provide a template like this:
* {{name}}
* {{age}}
* {{company}}
* {{{company}}}
... and we don't supply all the fields:
{
"name": "Chris",
"company": "<b>GitHub</b>"
}
... then by default any missing fields render as empty text (although this behavior is configurable in Mustache)::
* Chris
*
* <b>GitHub</b>
* <b>GitHub</b>
Mustache also provides support for escaping HTML (and Dhall does not), as the above example illustrates.
If we ignore the ability to escape HTML, then the corresponding Dhall template would be:
$ cat function1
\(record : { name : Text
, age : Optional Integer
, company : Text
}
)
-> ''
* ${record.name}
* ${Optional/fold Integer record.age Text Integer/show ""}
* ${record.company}
''
... and the corresponding data would be:
$ cat value1
{ name = "Chris"
, age = [] : Optional Integer
, company = "<b>GitHub</b>"
}
... which renders like this:
$ dhall-to-text <<< './function1 ./value1'
* Chris
*
* <b>GitHub</b>
Dhall forces us to declare which values are Optional
(such as age
) and which values are required (such as name
). However, we do have the luxury of specifying that individual values are Optional
, whereas Mustache requires us to specify globally whether all values are optional or required.
We also still have to supply an Optional
field, even if the field is empty. We can never omit a record field in Dhall, since that changes the type of the record.
We cannot interpolate record.age
directly into the string because the type of record.age
is Optional Integer
and not Text
. We have to explicitly convert to Text
, like this:
Optional/fold Integer record.age Text Integer/show ""
Informally, you can read this code as saying:
- If the
record.age
value is present, then useInteger/show
to render the value - If the
record.age
value is absent, then return the empty string
Optional/fold
is a builtin function that provides the most general function to consume an Optional
value. However, the type is a bit long:
∀(a : Type) -- The element type of the `Optional` value
→ Optional a -- The `Optional` value to consume
→ ∀(r : Type) -- The type of result we will produce
→ (a → r) -- Function to produce the result if the value is present
→ r -- Result if the value is absent
→ r
We can work through this large type by seeing what is the inferred type of Optional/fold
applied to successively more arguments:
Optional/fold
: ∀(a : Type) → Optional a → ∀(r : Type) → (a → r) → r → r
Optional/fold Integer
: Optional Integer → ∀(r : Type) → (Integer → r) → r → r
Optional/fold Integer record.age
: ∀(r : Type) → (Integer → r) → r → r
Optional/fold Integer record.age Text
: (Integer → Text) → Text → Text
Optional/fold Integer record.age Text Integer/show
: Text → Text
Optional/fold Integer record.age Text Integer/show ""
: Text
We could also make every field of the record optional, too:
\(record : { name : Optional Text
, age : Optional Integer
, company : Optional Text
}
)
-> let id = \(t : Text) -> t
in ''
* ${Optional/fold Text record.name Text id ""}
* ${Optional/fold Integer record.age Text Integer/show ""}
* ${Optional/fold Text record.company Text id ""}
''
... which would also require matching changes in the data:
{ name = ["Chris"] : Optional Text
, age = [] : Optional Integer
, company = ["<b>GitHub</b>"] : Optional Text
}
This is quite verbose, but we can take advantage of the fact that Dhall is a real programming language and define helper functions to reduce repetition. For example, we could save the following two files:
$ cat optionalText
\(x : Optional Text)
-> Optional/fold Text x Text
(\(t : Text) -> t) -- What to do if the value is present
"" -- What to do if the value is absent
$ cat optionalInteger
\(x : Optional Integer)
-> Optional/fold Integer x Text
Integer/show -- What to do if the value is present
"" -- What to do if the value is absent
... and then use those two functions to reduce the boilerplate of our template:
\(record : { name : Optional Text
, age : Optional Integer
, company : Optional Text
}
)
-> ''
* ${./optionalText record.name }
* ${./optionalInteger record.age }
* ${./optionalText record.company}
''
However, we might not even want to render the bullet at all if the value is missing. We could instead define the following two utilities:
$ cat textBullet
\(x : Optional Text)
-> Optional/fold Text x Text
(\(t : Text) -> "* ${t}\n")
""
$ cat integerBullet
\(x : Optional Integer)
-> Optional/fold Integer x Text
(\(t : Integer) -> "* ${Integer/show t}\n")
""
... and then we could write our template like this:
\(record : { name : Optional Text
, age : Optional Integer
, company : Optional Text
}
)
-> ./textBullet record.name
++ ./integerBullet record.age
++ ./textBullet record.company
... which would render like this:
* Chris
* <b>GitHub</b>
This illustrates how Dhall gives you greater precision in controlling the layout of your template. A template language like Mustache is limited by the fact that the templating logic must be expressed inline within the templated file itself. With Dhall you can separate the template from the logic if you want to avoid accidentally introducing superfluous newlines or whitespace.
Booleans
Mustache lets you guard a section of text to only display if a boolean value is True
:
Shown.
{{#person}}
Never shown!
{{/person}}
If you render that with this data:
{
"person": false
}
... then you get this result:
Shown.
The literal translation of that template to Dhall would be:
\(record : { person : Bool })
-> ''
Shown.
${ if record.person
then "Never shown!"
else ""
}
''
However, Dhall does not have to wrap everything in a record like Mustache does. We could just provide a naked Bool
argument to our function directly:
-- ./function2
\(person : Bool)
-> ''
Shown.
${ if person
then "Never shown!"
else ""
}
''
We also don't need to separate the argument out into a separate file. We can just apply the function directly to the argument like this:
$ dhall-to-text <<< './function2 False'
Shown.
... or we could combine both of them into the same file if we never intended to change the data:
let person = False
in ''
Shown.
${ if person
then "Never shown!"
else ""
}
''
Mustache also has a notion of "truthiness", meaning that you can use other types of values in place of boolean values. For example, the Mustache template permits person
to also be a List
or an Optional
value, and Mustache would treat the absence of a value as equivalent to False
and the presence of at least one value as equivalent to True
.
Dhall does not automatically treat Bool
/List
/Optional
as interchangeable. You have to explicitly convert between them in order to avoid type errors.
Lists
Mustache uses a similar syntax to render a list of values. For example, if you template this file:
{{#repo}}
<b>{{name}}</b>
{{/repo}}
... with this data:
{
"repo": [
{ "name": "resque" },
{ "name": "hub" },
{ "name": "rip" }
]
}
... then you would get this result:
<b>resque</b>
<b>hub</b>
<b>rip</b>
The equivalent Dhall template is:
let concatMap = https://ipfs.io/ipfs/QmRHdo2Jg59EZUT8Toq7MCZFN6e7wNbBtvaF7HCTrDFPxG/Prelude/Text/concatMap
in \(repo : List Text)
-> concatMap Text (\(name : Text) -> "<b>${name}</b>\n") repo
... and the equivalent Dhall payload is:
[ "resque"
, "hub"
, "rip"
]
Again, we don't need to wrap each value of the list in a one-field record like we do with Mustache. That's why we can get away with passing a list of naked Text
values (i.e. List Text
) instead of a list of one-field records (i.e. List { name : Text }
).
This example also illustrates how Dhall can import expressions by URL. Dhall hosts a Prelude of utilities online that you can use anywhere within your program by pasting their URL. The web is Dhall's "package system", except that instead of distributing code grouped in modules or packages you distribute code at the granularity of individual expressions.
The above example retrieves Dhall's concatMap
function from a URL hosted on IPFS (a distributed hashtable for the web). You don't have to use IPFS to distribute Dhall expressions, though; you can host code anywhere that can serve raw text, such as a pastebin, GitHub, or your own server.
Functions
Mustache also lets you supply user-defined functions, using the same syntax as for boolean values and lists. For example, you can template this file:
{{#wrapped}}
{{name}} is awesome.
{{/wrapped}}
... with this data:
{
"name": "Willy",
"wrapped": function() {
return function(text, render) {
return "<b>" + render(text) + "</b>"
}
}
}
... and Mustache will call the function on the block wrapped with the function's name:
<b>Willy is awesome.</b>
Dhall makes no distinction between functions and data because Dhall is a functional language where functions are first-class values. We can translate the above template to Dhall like this:
\(record : { wrapped : Text -> Text, name : Text })
-> record.wrapped "${record.name} is awesome"
... and translating the data to:
{ name = "Willy"
, wrapped = \(text : Text) -> "<b>${text}</b>"
}
Additional examples
We can translate the remaining examples from the Mustache manual fairly straightforwardly using the concepts introduced above.
Optional records in Mustache:
{{#person?}}
Hi {{name}}!
{{/person?}}
... translate to Optional
values in Dhall consumed using Optional/fold
:
\(person : Optional Text)
-> Optional/fold Text person Text
(\(name : Text) -> "Hi ${name}!")
""
The following inverted section in Mustache:
{{#repo}}
<b>{{name}}</b>
{{/repo}}
{{^repo}}
No repos :(
{{/repo}}
... is also just a special case of Optional/fold
in Dhall:
\(repo : Optional Text)
-> Optional/fold Text repo Text
(\(name : Text) -> "<b>${name}</b>")
"No repos :("
Inline template comments in Mustache:
<h1>Today{{! ignore me }}.</h1>
... are more verbose in Dhall:
"<h1>Today${"" {- ignore me -}}.</h1>"
What Mustache calls "partial" values:
$ cat base.mustache:
<h2>Names</h2>
{{#names}}
{{> user}}
{{/names}}
$ cat user.mustache:
<strong>{{name}}</strong>
... correspond to Dhall's support for importing paths as expressions:
$ cat base
let concatMap = https://ipfs.io/ipfs/QmRHdo2Jg59EZUT8Toq7MCZFN6e7wNbBtvaF7HCTrDFPxG/Prelude/Text/concatMap
in \(names : List Text)
-> ''
<h2>Names</h2>
${concatMap Text ./user names}
''
$ cat user
\(name : Text) -> "<strong>${name}</strong>"
Conclusion
If this interests you then you can test drive dhall-to-text
by installing the executable from Hackage or by building from source on GitHub.
People most commonly adopt Dhall when they prefer to use a programming language without sacrificing safety. Dhall is a total (i.e. non-Turing-complete) programming language, meaning that evaluation never crashes, hangs, throws exceptions, or otherwise fails.
Dhall also supports other programming features besides the ones introduced in this post. Read the Dhall tutorial if you would like to learn about the full set of features that Dhall supports.
Hi, you have a typo under your description of `--explain`: './functioin' should be './function'
ReplyDeleteThanks for catching that! I fixed it
Delete