dhall-clj

Dhall compiler to and from Clojure


Keywords
clojure, config, dhall
License
EPL-1.0

Documentation

dhall-clj

Build Status Build status Coverage Status Clojars Project cljdoc badge

Dhall + Clojure = 😍

This package allows you to compile Dhall expressions to Clojure expressions.

And this is a very useful thing! Why is it so?

Some use cases for Dhall are:

  • typed configurations: you need your configuration/data to have a precise type
  • templating: template things in a sane way (you definitely don't want a Turing complete templating language..)
  • send code on the wire in safe and efficient way: it's safe because the language is not Turing complete, and it's efficient because Dhall defines a binary encoding to serialize and deserialize expressions.

If this sounds useful to you too, then read on! For more inspiration about possible use cases, you can take a look at the wiki page "Using Dhall in Production" tracking the usage of Dhall in the wild.

Sounds nice! But where can I read more about this Dhall?

First of all, a quick definition: Dhall is a functional programming language that is not Turing complete, geared towards practical usage as a configuration language.

You can think of Dhall as: JSON + functions + types + imports.

For a more detailed pitch and a live demo, the Dhall website is the best place to start.
If you'd like a Dhall tutorial instead, see the README of the project, or the full tutorial.

Dhall Language Version and compliance to the Standard

Note: this is quite alpha. Things might be broken, so don't hesitate to open an issue.

Dhall has a versioned language specification, so it's useful to know which version we are using.

This library implements v5.0.0 of Dhall, except for http imports: that is, you cannot import things via http URLs yet.

Moreover, the translation to Clojure data structures is somewhat partial for now, so be extra careful when checking that the data you get from Dhall is what you expected.

For more information on the aspects in which this implementation is not compliant with the Standard, see the issues labeled with "standard compliance".

HOWTO

(require '[dhall-clj.core :refer [input input-ast]])

;; We can run compile and run Dhall expression in Clojure with the `input` function.
;; Note that the result of the evaluation is a Clojure value

(input "True && False")
;; => false


;; We can even import functions from Dhall..
;; (the following compiles a Dhall function into a Clojure function)

(def build-info (input "λ(major : Natural) → { version = \"${Natural/show major}.0\" }"))

;; ..and run them in Clojure!

(build-info 1)

;; => {"version" "1.0"}


;; Note that the `input` function calls `eval` on the imported form.
;; This is fine from the security point of view, as Dhall is not Turing Complete.
;; If you have any doubts, please read this document about Dhall's safety guarantees:
;; https://github.com/dhall-lang/dhall-lang/wiki/Safety-guarantees

;; However, if you would not like to emit & eval Clojure forms directly,
;; the function `input-ast` returns the AST constructed of the normalized expression:

(input-ast "1 + 1")

;; => #dhall_clj.ast.NaturalLit {:n 2}

HOWTO - Advanced usage

There are cases in which you'd need to run single compiler phases, e.g. if you wish to serialize expressions.

If we follow the lifetime of a Dhall expression being compiled, we'll see the following phases happening:

  • parsing: here we go from "text" to Abstract Syntax Tree (AST), that is going to be the representation we'll use for the Dhall expression in all the next phases.
    That is, all the following operations are defined as manipulations on the AST.
  • import resolution: a Dhall expression might contain local and remote imports (files, environment variables, http URLs), so we should try to resolve these imports and incorporate them into the expression before going forward (as we cannot verify the type of the expression if we don't know what the import will contain).
    So this phase will transform the AST containing imports into a import-free AST
  • typechecking: once we have the full expression, we have to verify that the types are correct, e.g. we'll check that if a function takes an Integer it's not being passed a Text, and so on.
  • execution/normalization: this is the final phase of the compilation, and effectively "runs" the computation. We do this by "reducing to a normal form" (all Dhall expression have a "normal form", and this means that all expressions always terminate), where we basically apply all the functions we can apply.
  • (optionally) emit Clojure: this phase is not strictly included in the compilation, but it will transform the Dhall AST from the normalization into Clojure datastructures. E.g. Dhall Lists will become Clojure vectors, Records will become Clojure maps, Dhall functions will become Clojure functions, etc.

Let's see how to use this knowledge to do interesting things (like serializing Dhall expressions to binary):

;; There are different namespaces for every compilation phase:
;; - "parsing"
(require '[dhall-clj.parse :refer [parse expr]])

;; - "import resolution" (the `state` ns is for the in-memory cache for imports)
(require '[dhall-clj.import :refer [resolve-imports]])
(require '[dhall-clj.state :as s])

;; - "typechecking"
(require '[dhall-clj.typecheck :refer [typecheck]])

;; - "normalization"
(require '[dhall-clj.beta-normalize :refer [beta-normalize]])

;; - "emit Clojure"
(require '[dhall-clj.emit :refer [emit]])

;; See the implementation of `dhall-clj.core/input` to see how to compose them together properly
;; But we can say that a basic re-implementation of `input` would be:

(defn my-input [dhall-text]
   (let [parse-tree (parse dhall-code)
         ast        (expr parse-tree)
         ast        (resolve-imports ast (s/new))
         type       (typecheck ast {})
         ast        (beta-normalize ast)
         clj-sexp   (emit ast)]
     (eval clj-sexp)))


;; Let's say we now want to serialize a Dhall expression to binary data.
;; We'll take the `build-info` function we used in the previous section to demonstrate this

(def dhall-source "λ(major : Natural) → { version = \"${Natural/show major}.0\" }")

(require '[dhall-clj.binary :refer [encode decode]])

(def serialized (-> dhall-source input-ast encode))

;; Now `serialized` will contain the serialized expression in a ByteArray.
;; You can now send this around, and when you want to _deserialize_ an encoded
;; expression you can run `decode` on it:

(def build-info (-> serialized decode emit eval))

;; ...and you can now run this code as well!

(build-info 1)

;; => {"version" "1.0"}

Catching exceptions

Sometimes the evaluation will fail, and we'd like to catch the error condition to handle that.

This library throws only ex-info, and the ex-data always includes a :type key.

This is so we can define a hierarchy of exceptions (which is defined in the dhall-clj.fail namespace), useful for catching groups of exceptions of a certain class, using the excellent ex library.

Example:

(require '[qbits.ex :as ex])

(ex/try+

  ;; The following will fail to typecheck
  (input "1 + 3.0")

  ;; This will catch the exact exception thrown by the above
  (catch-data :dhall-clj.fail/cant-add data
    (prn :cant-add data))

  ;; But if we didn't specify the exact match, we could still catch this
  ;; with the following, as `:dhall-clj.fail/cant-add` is a descendant of `:dhall-clj.fail/typecheck`
  (catch-data :dhall-clj.fail/typecheck data
    (prn :typecheck data))

  ;; And again if we did not catch the above, we could still catch the "library-wide"
  ;; exception type, as `:dhall-clj.fail/typecheck` is a descendant of `:dhall-clj.fail/dhall-clj`
  (catch-data :dhall-clj.fail/dhall-clj data
    (prn :dhall-clj data))

  ;; This would catch any ex-info not thrown by this library
  (catch ExceptionInfo e
    (prn :this-is-not-us e))

  ;; Catchall
  (catch Exception e))

License

Copyright © 2018 Fabrizio Ferrai

Distributed under the Eclipse Public License, the same as Clojure.