The jank programming language

jank is a general-purpose programming language which embraces the interactive, value-oriented nature of Clojure as well as the desire for native compilation and minimal runtimes. jank is strongly compatible with Clojure. Please note that jank is under heavy development; assume all features are planned or incomplete.

Where jank differs from Clojure is that its host is C++ on top of an LLVM-based JIT. Furthermore, jank has a built-in gradual type system which allows for malli-style type annotations which result in static type analysis. This allows jank to offer the same benefits of REPL-based development while being able to reach much further into the lands of both correctness and performance.

Still, jank is a Clojure dialect and thus includes its code-as-data philosophy and powerful macro system. jank remains a functional-first language which builds upon Clojure's rich set of persistent, immutable data structures. When mutability is needed, jank offers a software transaction memory and reactive agent system to ensure clean and correct multi-threaded designs.

Wide spectrum dynamics

Enjoy both dynamic typing and static typing gradually. Enjoy both REPL iteration with JIT compilation and static AOT compilation to native executables.


Iterate like you would with Clojure

As you iterate in the REPL and figure out your data shapes, static typing will not be in your way.

(defn unqualify
  "Strip the namespace from a keyword."
  (-> kw name keyword))

Add type annotations to lock down data shapes

Rather than using spec or malli to define your contracts, use jank's malli-like type definitions and then gain static type checking for any direct or indirect uses of that data.

(defn unqualify
  "Strip the namespace from a keyword."
  (-> kw name keyword))
(type unqualify [:fn [:keyword] :unqualified-keyword])

Compile to machine code

jank is built on an LLVM-based JIT. With AOT enabled, both statically and dynamically linked executables can be generated. The jank compiler itself has very speedy start times and low memory usage.

$ time jank hello-world.clj
hello world

real  0m0.101s
user  0m0.058s
sys   0m0.035s
$ time clj hello-world.clj
hello world

real  0m0.703s
user  0m1.957s
sys   0m0.109s

jank builds upon Clojure.

Keep your existing code; gain more confidence and more speed.

Strongly compatible with Clojure

Any Clojure library without interop will compile into your jank projects.

REPL and native JIT

Use your favorite nREPL editor plugin. jank uses an LLVM-based JIT to compile machine code on the fly.

Be gradual

Add types where you want them or disable static typing altogether. Generate dynamic binaries or static binaries, both using AOT compilation. Your choice.

Tooling friendly

Leiningen, LSP, nREPL planned from the start. jank's compiler is also written with tooling in mind, so it can be used for lexing, parsing, and analysis.

jank examples

All of the following examples are valid also Clojure code.

Generate a movie index

jank has very powerful capabilities for representing and transforming arbitrary data. Here, idiomatic usages of reduce, zipmap, repeat, and merge-with help create an index from genre to movie id with ease. No lenses are required for working with nested data.

(def movies {:the-weather-man {:title "The Weather Man"
                               :genres [:drama :comedy]
                               :tomatometer 59}
             :nightcrawler {:title "Nightcrawler"
                            :genres [:drama :crime :thriller]
                            :tomatometer 95}
             :the-bourne-identity {:title "The Bourne Identity"
                                   :genres [:action :thriller]
                                   :tomatometer 84}})

(def genre->movie (reduce (fn [acc [id movie]]
                            (let [{:keys [genres]} movie
                                  genre->this-movie (zipmap genres (repeat [id]))]
                              (merge-with into acc genre->this-movie)))

; genre->movie is now a useful index.
; =>
{:drama [:the-weather-man :nightcrawler],
 :comedy [:the-weather-man],
 :crime [:nightcrawler],
 :thriller [:nightcrawler],
 :action [:the-bourne-identity]}

; We can look up all movies by genre.
(->> (genre->movie :thriller)
     (map movies)
     (sort-by :tomatometer))
; =>
({:title "The Bourne Identity",
  :genres [:action :thriller],
  :tomatometer 84}
 {:title "Nightcrawler",
  :genres [:drama :crime :thriller],
  :tomatometer 95})

Convert bytes to human readable format

Beyond the traditional map, filter, and reduce, jank provides a powerful loop macro for more imperative-style loops while still being purely functional. Each loop has one or more corresponding recur usages which must be in tail position.

(defn size->human-readable
  "Converts a size, in bytes, to a human readable format, such as 0 B, 1.5 kB,
   10 GB, etc."
  (if (and (< -1000 size-in-bytes) (< size-in-bytes 1000))
    (str size-in-bytes " B")
    (let [res (loop [acc size-in-bytes
                     suffixes "kMGTPE"]
                (if-not (or (<= acc -999950) (<= 999950 acc))
                  {:size acc
                   :suffix (first suffixes)}
                  (recur (/ acc 1000) (drop 1 suffixes))))]
      (format "%.1f %cB" (float (/ (:size res) 1000)) (:suffix res)))))

(assert (= "0 B" (size->human-readable 0)))
(assert (= "57.0 kB" (size->human-readable (* 57 1000))))

Truncate a string to a max length

jank's strings, as well as most of its other data structures, are immutable. However, jank provides such powerful tools for working with data that mutability is very rarely a concern.

(def max-text-length 256)
(defn truncate
  "Truncates the text to be no longer than the max length."
  [text max-length]
    (<= max-length 0)

    (<= (count text) max-length)

    (str (subs text 0 (dec max-length)) "…")))

(assert (= "" (truncate "wowzer" 0)))
(assert (= "wow…" (truncate "wowzer" 4)))

Redefine any var

Every def or defn exists within a var, which is a stable, namespace-level container for values. Vars can be redefined to contain different values. with-redefs redefines a var within its body's scope, which is very useful for removing side effects from test cases or forcing functions to return specific values.

(defn post! [_request]
  ; Assuming this performs some network effect.
  {:status 200
   :body (pr-str {:order 7821})})

(defn submit-order! []
  (let [request {:url "/submit-order"}
        order (post! request)
        order-body (-> order :body read-string)]
    (if (contains? order-body :error)
      ; This is the code path we want to test.
      {:error "failed to submit"
       :request request
       :response order-body}
      (:order order-body))))

; Later on, in tests, skip the side effect by redefining.
(deftest submit-order
  (testing "failed post"
    (with-redefs [post! (fn [_]
                          ; Fake error to see how the rest of the code handles it.
                          {:status 500
                           :body (pr-str {:error "uh oh"})})]
      (is (= "failed to submit" (:error (submit-order!)))))))