Dhall is a programmable configuration language with a type system. One of its core features is the merge function, which provides exhaustive pattern matching on union types (sum types). If you forget a case, Dhall rejects the configuration at evaluation time.
Union Types: A Quick Review
A union type represents a value that can be one of several variants. In Haskell:
data Shell = Bash | Zsh | FishA value of type Shell is Bash, Zsh, or Fish. Pattern matching lets you handle each case:
shebang :: Shell -> String
shebang Bash = "#!/bin/bash"
shebang Zsh = "#!/bin/zsh"
shebang Fish = "#!/bin/fish"The compiler verifies you handled all cases. Miss one, and you get a warning (or error with -Werror).
Dhall's merge Function
Dhall has no pattern matching syntax. Instead, it provides merge, which takes a record of handlers (one per variant) and a union value:
let Shell : Type = < Bash | Zsh | Fish >
let shebang : Shell -> Text =
\(shell : Shell) ->
merge
{ Bash = "#!/bin/bash"
, Zsh = "#!/bin/zsh"
, Fish = "#!/bin/fish"
}
shell
in shebang Shell.Zsh
-- Evaluates to: "#!/bin/zsh"The record keys must match the union's variants. Dhall checks this at evaluation time. If you add a variant to Shell but forget to update the merge record, Dhall rejects the expression.
Union Types with Payloads
Union variants can carry data. In Haskell:
data Environment
= Prod
| QA Text -- story ticket ID
| Dev Text -- developer identifierPattern matching extracts the payload:
logServer :: Environment -> Text
logServer Prod = "logs.prod.example.com"
logServer (QA _) = "logs.qa.example.com"
logServer (Dev tag) = "logs." <> tag <> ".local"Merge with Payloads in Dhall
In Dhall, variants with payloads are declared with a type annotation. The handler becomes a function:
let Environment : Type = < Prod | QA : Text | Dev : Text >
let logServer : Environment -> Text =
\(env : Environment) ->
merge
{ Prod = "logs.prod.example.com"
, QA = \(ticket : Text) -> "logs.qa.example.com"
, Dev = \(tag : Text) -> "logs." ++ tag ++ ".local"
}
env
in logServer (Environment.Dev "mbbx6spp")
-- Evaluates to: "logs.mbbx6spp.local"Note the difference:
Prodhas no payload, so its handler is a plain valueQAandDevhaveTextpayloads, so their handlers are functions
Type Safety in Practice
Dhall's merge provides the same guarantees as Haskell's pattern matching:
- Exhaustiveness: Every variant must have a handler
- Type correctness: Each handler must return the same type
- Payload types: Handler functions must accept the declared payload type
This makes refactoring safe. Add a new variant to your union type, and Dhall will reject every merge that does not handle it.
When to Use Union Types
Union types model mutually exclusive options:
- Environment tiers (dev, staging, prod)
- Feature flags with configuration
- Protocol message types
- Error categories
The merge function turns these into computed values: hostnames, feature settings, timeout durations, or any other derived configuration.
Full Example: Feature Flags
let Feature : Type =
< Enabled
| Disabled
| RolloutPercent : Natural
>
let isEnabled : Feature -> Bool =
\(f : Feature) ->
merge
{ Enabled = True
, Disabled = False
, RolloutPercent = \(pct : Natural) -> Natural/isZero pct == False
}
f
let timeout : Feature -> Natural =
\(f : Feature) ->
merge
{ Enabled = 30
, Disabled = 0
, RolloutPercent = \(_ : Natural) -> 30
}
f
in { cacheEnabled = isEnabled Feature.Enabled
, cacheTimeout = timeout Feature.Enabled
, betaEnabled = isEnabled (Feature.RolloutPercent 25)
}
-- Evaluates to: { cacheEnabled = True, cacheTimeout = 30, betaEnabled = True }The merge function is Dhall's answer to pattern matching. It provides exhaustiveness checking without special syntax, using records and functions you already understand.