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 and considers itself a dialect of Clojure. Please note that jank is under heavy development; assume all features are planned or incomplete.

Where jank differs from Clojure JVM is that its host is C++ on top of an LLVM-based JIT. This allows jank to offer the same benefits of REPL-based development while being able to seamlessly reach into the native world and compete seriously with JVM's 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 REPL iteration with JIT compilation and static AOT compilation to native executables.

01

Iterate like you would with Clojure

Iterate in the REPL and build your program from the ground up without leaving your editor.

(defn -main [& args]
  (loop [game-state (new-game!)]
    (when (done? game-state)
      (end-game! game-state)
      (recur (next-state game-state)))))
02

Reach into the native world

Seamlessly switch to inline C++ within your Clojure source, while still having access to your Clojure code using interpolation.

(defn create-vertex-shader! []
  (native/raw "__value = make_box(glCreateShader(GL_VERTEX_SHADER));"))

(defn set-shader-source! [shader source]
  (native/raw "auto const shader(detail::to_int(~{ shader }));
               auto const &source(detail::to_string(~{ source }));
               __value = make_box(glShaderSource(shader, 1, &source.data, nullptr));"))

(defn compile-shader! [shader]
  (native/raw "__value = make_box(glCompileShader(detail::to_int(~{ shader })));"))
03

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.

Go native

Reach into native libraries or interact directly with your native code base. Seamlessly write both C++ and Clojure in the same file.

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 also valid 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)))
                          {}
                          movies))

; genre->movie is now a useful index.
; =>
{:drama [:the-weather-man :nightcrawler],
 :comedy [:the-weather-man],
 :crime [:nightcrawler],
 :thriller [:nightcrawler :the-bourne-identity],
 :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."
  [size-in-bytes]
  (if (< -1000 size-in-bytes 1000)
    (str size-in-bytes " B")
    (let [res (loop [acc size-in-bytes
                     suffixes "kMGTPE"]
                (if (< -999950 acc 999950)
                  {: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]
  (cond
    (<= max-length 0)
    ""

    (<= (count text) max-length)
    text

    :else
    (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!)))))))