Best practice for using dhall as configuration format for a simple application


#1

Hello everybody,

I‘d like to experiment a bit with Dhall. I want to use it as the configuration format file for my simple app. Right know my needs are really simple, just a few fields assigning texts, bools and enumerations. And I am sure that I can patch together some solution in Haskell with the existing library and documentation.

But I kinda wanna learn how to do this proper. My aim is

  1. for the user of my app to be able to write a configuration as comfortable as if I where using a more traditional format like ini or toml
  2. them to be able to type-check against my provided schema and all the programing advantages, if they want to.

So is there somewhere a good tutorial or even library for:

  1. Supply a schema to my users to make type-checking easy.
  2. Supply type definitions. Especially with union types, for the user to use in their configuration.
  3. Let the user only write the values where they want to deviate from the defaults and use the defaults for the rest. Make some fields mandatory others not.
  4. Give the configuration-file via command line, fall back to $MY_APP_CONFIG_FILE, fall back to $XDG_HOME_CONFIG/my-app/config.dhall, fall back to ~/.config/my-app/config.dhall, fall back to default config. (Or something similar.)
  5. Make single options overridable via environment variables and/or command line variables.

Of course I don‘t expect you to solve that for me. But it sounds very generic so maybe some one has already done at least parts of it.

What mainly has hold me back until now is to decide which parts of it I want to implement in the host language (Haskell) and which part in Dhall. Dhall e.g. can read env variables but can it read $XDG_HOME_CONFIG/my-app/config.dhall? I haven‘t figured out how.I guess I can write most of the fallback behaviour in dhall, but I don‘t know how to mix-in the command line variables.

For supplying the Type definitions, I can maybe do this nix-style and let the user write a function which gets the enumeration types as a record?

If the solution includes a non-trivial dhall expression into the source code of my app which then would load other files and also include the default config: Are there any attempts for type- or at least syntax-checking at compile-time of my app. Wouldn‘t want dhall code that I ship to throw a runtime error.

Sorry for the very fuzzy question. Any partial answers or ways in which you think I should approach this differently are appreciated.


#2

You might be interested in the Dhall Manual that I’m writing, which is designed to document best practices like these. Some of the questions you’ve raised are already covered there, but some are not, so I’ll link to the manual when appropriate and use your questions to inspire additional chapters.

You might also be interested in the Language Tour, too, as some of your questions are answered by that as well. That was a tutorial that we very recently added.

I’ll interpret (1) to mean that you want to provide a Dhall type to the user that they can use to valid their configuration ahead of time using a type annotation and the dhall command-line tool.

There are two general ways to do that:

  • You can make the Haskell type the source of truth and generate the corresponding Dhall type from the Haskell type

    You can do this using:

    Dhall.Core.pretty (Dhall.expected (Dhall.auto @YourHaskellType))
    

    … which will pretty-print the Dhall type corresponding to YourHaskellType

  • You can make the Dhall type the source of truth and generate the corresponding Haskell type from the Dhall type

    You can do this using the Dhall.TH module, which has more details:

    https://hackage.haskell.org/package/dhall/docs/Dhall-TH.html

If you have the option to choose then I generally recommend the latter approach (make the Dhall type the source of truth). This is generally standard practice in other interface definition languages (IDLs) like protocol buffers or Thrift because it avoids enshrining any particular language binding.

So the next question is how to make the Dhall type available to your users. The simplest way is to host your types on GitHub. The manual has a chapter that is partially related:

… although more details on how to author packages is intended for the latter half of the manual. Part of the reason is that we’re still building out the infrastructure for more principled way of hosting packages (like a documentation generator and a Dhall-aware package hosting service), but currently GitHub is the de facto way to host Dhall packages.

For (3), there is a section of the manual which is highly relevant, which is:

There is also a section from the Language Tour on the record completion operator, too, albeit with a less worked example:

The short summary is that the language supports a record completion operator that you can design your types to support so that users only need to specify non-default values.

For (4), the Language Tour has a section on the alternative operator which is designed for exactly this purpose:

… and the direct answer to your question is you can get pretty close to what you want using:

env:MY_APP_CONFIG_FILE ? ~/.config/my-app/config.dhall ? defaultConfig

The only thing Dhall does not let you do is to import from $XDG_HOME_CONFIG/my-app/config.dhall because the language does not support “computed imports”, for security reasons. For more details on safety guarantees, see:

For (5), you can optionally override a record field using an environment variable like this:

let exampleInputRecord : { name : Text, age : Natural } = …

in  exampleInputRecord // ({ name  = env:NAME as Text } ? {=})

If the NAME environment variable is unset then the ? operator will fall back to the next available choice.

However, there is no way that the Dhall program can access command-line values. The Haskell program will have to wire them in, somehow. There isn’t a best practice here, yet, mainly because nobody has asked about this until now so I haven’t given this enough thought.

Note that if you need Nix support for packaging Dhall expressions, I already built that out. See:

That allows you to use Nix to build “offline” Dhall packages instead of using Dhall’s HTTP import system. This can come in handy in a restricted environment where you want only the build system (Nix in this case) to make HTTP requests instead of making them at runtime. Also, if you are interested in generalizing this to other build systems, I wrote up more detail on how to do that here:

There isn’t yet a formal document outside of those two that you can reference, but I’m working on it (starting with adding a Dhall section to the Nixpkgs manual). If you need more help using that, I can go into more detail.

You can validate Dhall code ahead of time using the dhall command-line tool. The most common commands that you’ll find relevant are:

$ dhall --file ./example.dhall  # Interpret a file
$ dhall type --quiet --file ./example.dhall  # Check the file for type errors
$ dhall type --quiet <<< './example.dhall : ./schema.dhall'  # Check the file against a schema
$ dhall hash --file ./example.dhall  # Compute a semantic hash of the file (useful for refactoring)

The Language Tour walks through the command in a little more detail, but there isn’t yet a comprehensive guide to the command-line tooling, yet.

The dhall tool is included with the Haskell package and you or your users can download pre-built executables for Windows, OS X, and Linux from this release page:

We’ve also packaged Dhall for Brew and Nix if you use those package managers. I know that Arch has also packaged Dhall on their own accord, but I don’t think other Linux distros (like Fedora Ubuntu) have packaged Dhall yet.


#3

This is basically what I do in my Haskell app. There is a bunch of config fields (mostly Text), with users defining a subset of them (the rest will use application defaults).

See Dhall for simple application configuration.