jank programming language - Clojure/LLVM/C++

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!)))))))