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.

Susan Potter

Susan Potter

Quant

Work with me

I spent the first half of my career building risk models and market data infrastructure at BNP Paribas, Bank of America, and Citadel, then fourteen years shipping production systems at scale. Now I bring both sides to quantitative trading. If you're a trading firm, family office, or fund looking to tighten the connection between your research ideas and your production trading systems, whether that's building validation pipelines, formalizing signal logic, or getting microstructure analytics into a deployable state, I'd like to hear what you're working on. Reach me at me@susanpotter.net.