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.
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)))))
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 })));"))
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
Any Clojure library without interop will compile into your jank projects.
Use your favorite nREPL editor plugin. jank uses an LLVM-based JIT to compile machine code on the fly.
Reach into native libraries or interact directly with your native code base. Seamlessly write both C++ and Clojure in the same file.
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 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})
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))))
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)))
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!)))))))