Susan Potter
### snippets  ·  Created  ·  Updated

Dhall's merge: Pattern Matching for Configuration

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 | Fish

A 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 identifier

Pattern 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:

  • Prod has no payload, so its handler is a plain value
  • QA and Dev have Text payloads, so their handlers are functions

Type Safety in Practice

Dhall's merge provides the same guarantees as Haskell's pattern matching:

  1. Exhaustiveness: Every variant must have a handler
  2. Type correctness: Each handler must return the same type
  3. 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.