Reuse

The simplest use of dhall-hpack-cabal is to ingest one self-contained package.dhall file but by using imports and functions we can do more than just create a single output, the .cabal file.

The trick with reuse is to break apart package.dhall into sections for reuse.

Section reuse is a facility Cabal has with its common stanzas that can be imported by other common stanzas and by components in the package description .cabal file. Cabal restricts what can be commonly shared to build information. That’s a lot of fields but there are also a lot of fields that are excluded, such as the author and maintainer, copyright and license-related fields.

Section reuse is a facility YAML has too with its & anchors, * aliases, and << merge keys.

What neither Cabal’s .cabal files nor standard YAML .yaml files have is the ability to include or import from another file but Hpack’s facilities for reuse support the non-standard YAML !include directive by its use of Data.Yaml.Include.decodeFileWithWarnings, a function that does file inclusions by raw text insertion into the stream without validation.

Dhall Import Advantages

Dhall’s imports are great. They provide strongly typed, secure, and composable configuration and have a lot of saftey guarantees. Dhall imports have (or optionally can have):

  • Type Checking Across Files Imports are type-checked before evaluation and it is possible to mark imported data with a separate imported type, something other data formats achieve with schemas.

  • Security It is possible to dhall freeze, which computes a SHA256 hash of the imported content.

  • Programmable Composition Imports are expressions. You can import a function and apply it to data or import data and pass it into a function.

  • Termination Dhall is not Turing-complete. Importing will never hang or crash the parser.

Author and Maintainer Details

If a single individual or company is authoring and maintaining a lot of packages then they might like to define their author and maintainers details once and import those. They might put all their source code under the same GitHub organisation and always use the same license but put each package in its own repository and be prepared for changing copyright years.

author.dhall
\(copyright-years : Text) ->
  \(repo : Text) ->
      { author = "Thaddeus Blackwood"
      , maintainer = "thad@black-wood.invalid"
      , copyright = "© ${copyright-years} Thaddeus Blackwood"
      , github = "thad-black-wood/${repo}"
      , license = "MPL-2.0"
      , license-file = "LICENSE.md"
      , extra-source-files = [ "LICENSE.md" ]
      }

Dual Targets

If the default extensions are in their own default-extensions.dhall file, then we can generate a .yaml file equivalent and use this for configuring the extensions that HLint turns on when linting.

default-extensions.dhall
{ default-extensions =
    [ "DataKinds"
    , "DeriveFunctor"
    ...
    , "TupleSections"
    , "UndecidableInstances"
    ]
}

We want to generate a .yaml file with an arguments list of extensions. The Dhall function to do this is but a few lines long. It prefixes each extension with “-X”:

hlint.dhall
let Prelude/List/map =
          https://raw.githubusercontent.com/dhall-lang/Prelude/35deff0d41f2bf86c42089c6ca16665537f54d75/List/map

in  let defs = ./default-extensions.dhall

in  let f = λ(s : Text)  "-X" ++ s

in  { arguments = Prelude/List/map Text Text f defs.default-extensions }
Note

Imports can be URIs or local files. The example above has both kinds.

Relative imports have to start with “./” or “../”.

We generate an .hlint.yaml file (or you could create an .hlint-extensions.yaml file and import it) with dhall-to-yaml < ./hlint.dhall > ./.hlint.yaml.

.hlint.yaml
arguments:
- -XDataKinds
- -XDeriveFunctor
# ...
- -XTupleSections
- -XUndecidableInstances

We can pull those same default-extensions into a package description:

package.dhall
let defs = ./defaults.dhall

in    defs
     ./default-extensions.dhall
     { name = "flight-units"
      , github = "blockscope/flare-timing/units"
      , dependencies =
            defs.dependencies
          # [ "numbers"
            , "fixed"
            , "bifunctors"
            , "text"
            , "formatting"
            , "uom-plugin"
            , "siggy-chardust"
            ]
      }

It doesn’t really matter for this example what defaults.dhall contains except that it too has dependencies. If it has its own default-extensions then those are merged with :

defaults.dhall
{ default-extensions =
    [ "PackageImports" ]
, dependencies =
    [ "base >=4.10.1.0 && <5" ]
}