Home

Awesome

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:

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:

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.