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.
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 }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
- -XUndecidableInstancesWe 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" ]
}