Introduction

Hpack-Dhall brings functions, types and file imports to Haskell packaging. It is Hpack with Dhall.

Hpack is “a modern format for Haskell packages” and Dhall “is a programmable configuration language that you can think of as: JSON + functions + types + imports”.

The purpose of Hpack-Dhall’s executables is to generate one or more package-name.cabal files from package.dhall input files. Hopefully this is both easier and less prone to errors and omissions than writing the .cabal files by hand. The two main executables of the hpack-dhall package are dhall-hpack-cabal and its Cabal command extension companion cabal-dpack (callable as cabal dpack):

  • dhall-hpack-cabal: From one package.dhall generates one <package-name>.cabal.
  • cabal-dpack: same thing, but for any package.dhall in the current directory or any subdirectory.

Here’s the list of all included executables:

$ cabal target all:exes
Fully qualified target forms:
 - hpack-dhall:exe:cabal-diy-pack
 - hpack-dhall:exe:cabal-dpack
 - hpack-dhall:exe:cabal-ypack
 - hpack-dhall:exe:dhall-hpack-cabal
 - hpack-dhall:exe:dhall-hpack-dhall
 - hpack-dhall:exe:dhall-hpack-json
 - hpack-dhall:exe:dhall-hpack-yaml
 - hpack-dhall:exe:yaml-hpack-cabal
Found 8 targets matching all:exes.

If you’re migrating from hpack to Hpack-Dhall then dhall-hpack-yaml can be used to generate a package.yaml. This could be easier than trying to match the generated package-name.cabal.

If for some reason you want JSON, then dhall-hpack-json does that.

For seeing what a package.dhall would look like with imports resolved there’s dhall-hpack-dhall.

The three included Cabal external commands are; cabal-dpack, cabal-ypack and cabal-diy-pack.

Installation

All executables can be installed from the Hackage-published package with:

$ cabal install hpack-dhall

If building from source, all executables or just some of them1 can be installed with:

$ cabal install all:exes --overwrite-policy=always

Benefits

A lot of the benefits are those of Hpack, like for instance, expanding globs in extra-source-files. It is ergonomic for the lazy typist, relying on hpack to fill in details.

You can apply functions to alter the generated .cabal package description.

If you maintain multiple packages then you can share some of the package configuration between them by importing one or more common .dhall files. Imported .dhall files can be types, can be data or they can be functions we apply to arguments.

There’s no need of a separate formatter for the generated .cabal file.

dhall format --help
Usage: dhall format [--transitive] [--check] [FILES]

  Standard code formatter for the Dhall language

Available options:
  --transitive             Modify the input and its transitive relative imports
                           in-place
  --check                  Only check if the input is formatted
  FILES                    Read expression from files instead of standard input
  -h,--help                Show this help text

Drawbacks

Drawbacks include those of Hpack compared to going with Cabal’s Package Description .cabal files and typing problems related to having to fit together typed-Dhall with the untyped-YAML of Hpack.

We use the phraseology of Hpack. This is YAML, a string-ish language with its keys and simple or nested values and its many syntactic forms for the same data. Cabal too has various syntactic forms representing the same data.

Cabal’s field list values can be space separated, comma separated, nested or not or they may use set syntax:

field: a b
field:
    a
  , b
field: {a, b}

YAML’s field lists can be inline or nested:

field: [a, b]
field:
  - a
  - b

The problem with going with a different phraselogy than Cabal’s is that you’ll often need to consult the Hpack docs to see which field name to use because they’re sometimes different than Cabal’s field names:

- build-depends
+ dependencies

You’re on your own working out which type to use in Dhall but that gives some freedom to choose. Hpack’s types are often strings or lists of strings and all that really matters is that when the Dhall is converted to JSON and feed to Hpack is that it can interpret those values and write the .cabal file.

Hpack doesn’t support Cabal’s common stanzas. Where Hpack doesn’t support a Cabal feature, there is the verbatim escape route that allows use to write “whatever” into the generated .cabal file.

Hpack has automatic module discovery. That’s all good except when you want to exclude some modules in a folder from being discovered. In that case, we either keep only modules we want discovered in the module tree or are forced to be more explicit about which modules are in and which are out, either by forgoing module discovery and being explicit with exposed-modules and other-modules or by hiding modules from discovery by putting them behind a false condition.

library:
  when:
  - condition: false
    other-modules: [modules to hide go here]

Hpack is pretty lenient with how dependencies can be supplied2; as a list, an object or even where items are themselves objects with name and version. This won’t fly with a Dhall encoding. Putting dependencies or any other configuration in a list requires each element to be of the same type.

Alternatives

There is another Haskell packaging solution using Dhall, dhall-to-cabal, started by Ollie Charles and moved to the dhall-lang organisation in Feb 2018. This takes a more strongly-typed stance than Hpack-Dhall. Since late 2019, Ollie doesn’t use the project any more and has stepped away from maintainership.

The dhall-to-cabal package includes the cabal-to-dhall executable. This is able to convert quite complex .cabal files, such as Stack’s own stack.cabal, to .dhall with a type but the result is very verbose. There’s no one that would want to type these 94,028 lines by hand!

examples/stack/cabal-to-dhall.dhall
let prelude =
      https://raw.githubusercontent.com/dhall-lang/dhall-to-cabal/1.3.4.0/dhall/prelude.dhall

let types =
      https://raw.githubusercontent.com/dhall-lang/dhall-to-cabal/1.3.4.0/dhall/types.dhall

in    prelude.defaults.Package
    ⫽ { name =
          "stack"
      , version =
          prelude.v "2.12.0"
      , author =
          "Commercial Haskell SIG"
      , custom-setup =
          Some
          { setup-depends =
              [ { bounds =
                    prelude.earlierVersion (prelude.v "3.12")
                , package =
                    "Cabal"
                }
              , { bounds =
                    prelude.intersectVersionRanges
                    ( prelude.unionVersionRanges
                      (prelude.thisVersion (prelude.v "4.14.3.0"))
                      (prelude.laterVersion (prelude.v "4.14.3.0"))
                    )
                    (prelude.earlierVersion (prelude.v "5"))
                , package =
                    "base"
                }
              , { bounds = prelude.anyVersion, package = "filepath" }
              ]
          }
      }

Describing a package with dhall-to-cabal can be done tersely3, by using fewer qualified names, formatting fields more horizontally (but this means avoiding dhall format) and using less verbose version specifications, such as seen in this diff between the cabal-to-dhall generated dependency on base and a hand-written equivalent:

- { bounds =
-       prelude.intersectVersionRanges
-       ( prelude.unionVersionRanges
-       ( prelude.thisVersion
-           ( prelude.v
-           "4.14.3.0"
-           )
-       )
-       ( prelude.laterVersion
-           ( prelude.v
-           "4.14.3.0"
-           )
-       )
-       )
-       ( prelude.earlierVersion
-       ( prelude.v
-           "5"
-       )
-       )
-   , package =
-       "base"
- }
+ package
+   "base"
+   ( intersectVersionRanges
+       (orLaterVersion (v "4.14.3"))
+       (earlierVersion (v "5"))
+   )

Perhaps dhall-to-cabal needs its own Hpack-like tool for humans to ergonomically interact with that in turn writes .dhall with a dhall-to-cabal type?

Footnotes

  1. Despite selecting one executable with cabal install cabal-dpack it installs all of them and this is a bug.↩︎

  2. Hpack’s dependencies section show examples of each style, including an example that mixes styles.↩︎

  3. Its own package, dhall-to-cabal.dhall, is fairly terse.↩︎