On Github peter / clojure-api-slides
Created by Peter Marklund / @peter_marklund
Programming language for the JVM, JavaScript et al
Lisp, dynamically typed, functional with emphasis on pure functions, data-oriented
Released in 2007
Simplicity is a prerequisite for reliability
The complexity of our systems dominate our ability to be productive in the long run
Programming languages differ not in what they make possible but in what they make practical and idiomatic
Number of bugs is the number of lines of code to the power of 1.5
By choosing data instead of custom objects we get:
(defn greeting [name] (str "Hello " name)) (def greeting (fn [name] (str "Hello " name))) (def greeting #(str "Hello " %))functions can be named or anonymous. Anonymous functions can be written in two ways. All function definitions are equivalent.
(greeting "Joe") ; => "Hello Joe" (greeting) ; => ArityExceptionfunctions can be named or anonymous. Anonymous functions can be written in two ways. All function definitions are equivalent.
(defn greeting ([name] (str "Hello " name)) ([] (greeting "World"))) (greeting) ; => "Hello World" (greeting "Joe") ; => "Hello Joe"
(defn complement [f] (fn [& args] (not (apply f args)))) (def not-nil? (complement nil?)) (not-nil? "foobar") ; => trueThis is also an example of a higher order function that returns a function.
(let [a 1 b 2] (+ a b)) ; => 3 (defn foo [c] (let [a 1 b 2] (+ a b c))) (foo 3) ; => 6
(def thing "global") (let [thing "local"] thing) ; => "local" thing ; => "global"
(defn authenticate [email password] (if (and (= email "open") (= password "sesame")) "token" nil)) (defn login [request] (let [email (get-in request [:params :email]) password (get-in request [:params :password]) token (authenticate email password)] (if token {:status 200 :body {:token token}} {:status 401}))) (login {:params {:email "open" :password "sesame"}}) ; => {:status 200 :body {:token "token"}}
123 ; java.lang.Long 1.23 ; java.lang.Double "foobar" ; java.lang.String true, false ; java.lang.Boolean nil ; java null #"fo.*bar" ; java.util.regex.Pattern my-variable ; clojure.lang.Symbol :my-keyword ; clojure.lang.Keywordthese are the usual Java types java.lang.Long/Double/String/Boolean
'(1 "foobar" 3); clojure.lang.PersistentList [5 nil 3] ; clojure.lang.PersistentVector {:a 1 :b 2} ; clojure.lang.PersistentArrayMap #{:foo :bar} ; clojure.lang.PersistentHashSetLists are singly linked and add efficiently at the front. Vectors are functions that take an index and return a value. Maps are functions that take a key and return a value. All data structures can hold heterogeneous data and can be nested. Lists and vectors have a sequential shape. Hash maps and sets are associative. Vectors are also associative on the index and work with assoc.
([5 nil 3] 2) ; => 3 (nth [5 nil 3] 1 :default) ; => nil ({:a 1 :b 2} :b) ; => 2 (:b {:a 1 :b 2}) ; => 2 (get {:a 1 :b nil} :b :default) ; => nil (#{:foo :bar} :bar) ; => :bar (contains? #{:foo :bar} :bar) ; => true
(for [color [:red :blue] animal [:mouse :duck]] (str (name color) " " (name animal))) ; => ("red mouse" "red duck" "blue mouse" "blue duck") (for [n (range 10) :when (even? n)] n) ; => (0 2 4 6 8)
(defn my-map [f coll] (if-let [s (seq coll)] (cons (f (first coll)) (my-map f (rest coll))))) (my-map #(* % 2) [1 2 3 4])
(defn my-map [f coll] (if-let [s (seq coll)] (cons (f (first coll)) (my-map f (rest coll))))) (my-map #(* % 2) (range 100000)) ; => StackOverflowError
(defn my-map [f coll] (let [map-fn (fn [f coll acc] (if-let [s (seq coll)] (recur f (rest coll) (conj acc (f (first coll)))) acc))] (map-fn f coll []))) (my-map #(* % 2) (range 100000)) ; => [0 2 4 6 8 10 ...]
(defn every? "Returns true if (pred x) is true for every x in coll" [pred coll] (cond (nil? (seq coll)) true (pred (first coll)) (recur pred (next coll)) :else false))
(defn zipmap "Returns a map with the keys mapped to vals" [keys vals] (loop [map {} ks (seq keys) vs (seq vals)] (if (and ks vs) (recur (assoc map (first ks) (first vs)) (next ks) (next vs)) map)))Notice the use of first and next which is typical in recursive code. One reason to use seq here is that an empty seq is nil and thus false in a boolean context. Also notice the use of a documentation string for functions.
map/filter/remove/reduce conj/cons first/second/last/next not-empty every?/not-any?/some count take/drop reverse partition/flatten interleave/interpose group-by sort-by distinct doseq ...
(def keys [:a :b]) (def values [1 2]) (into {} (map vector keys values)) (apply hash-map (interleave keys values)) (zipmap keys values) ; => {:a 1 :b 2} (reduce (fn [m [k v]] (assoc m k v)) {} (partition 2 [:a 1 :b 2])) ; => {:a 1 :b 2}
(defn csv-map [lines] (map #(zipmap (first lines) %) (rest lines))) (csv-map [["Name" "Age"] ["Sven" 9] ["Anna" 6]]) ; => ({"Name" "Sven", "Age" 9} {"Name" "Anna", "Age" 6})
(assoc {:a 1} :b 2) ; => {:a 1, :b 2} (dissoc {:a 1 :b 2} :b) ; => {:a 1} (get-in {:a {:b 1}} [:a :b]) ; => 1 (update-in {:a {:b 1}} [:a :b] inc) ; => {:a {:b 2}} (update-in {:a} [:a :b] (fnil inc 0)) ; => {:a {:b 0}} (select-keys {:a 1 :b 2} [:b]) ; => {:b 2} (clojure.set/rename-keys {:a 1} {:a :b}) ; => {:b 1} (merge {:a {:b 1}} {:a {:c 2}}) ; => {:a {:c 2}} (assoc [:foo :bar] 3 :baz) ; => IndexOutOfBoundsException
(defn map-values [f dict] (zipmap (keys m) (map f (vals m)))) (defn map-values [f m] (reduce (fn [r [k v]] (assoc r k (f v))) {} m)) (map-values (partial * 2) {:a 1 :b 2}) ; => {:a 2, :b 4}
(defn deep-merge "Recursively merges maps" [& vals] (if (every? map? vals) (apply merge-with deep-merge vals) (last vals))) (deep-merge {:a {:b 1}} {:a {:c 2}}) ; => {:a {:b 1 :c 2}}
(clojure.set/union #{:a :b} #{:b :c}) ; => #{:c :b :a} (clojure.set/intersection #{:a :b} #{:b :c}) ; => #{:b} (clojure.set/difference #{:a :b} #{:b :c}) ; => #{:a}
(re-matches #"^foo.*" "foobar") ; => "foobar" (re-seq #"\w+" "Hello World") ; => ("Hello" "World") (str/split "A fine day it is" #"\W+") (str/replace "foobar foobar" #"(foo)(bar)" "$2$1") ; => "barfoo barfoo" (str "Hello" " " "World") ; => "Hello World" (format "Hello there, %s" "bob") ; "Hello there, bob" ; lower-case, blank?, starts-with?/ends-with?, includes?
Value semantics for all data types and structures
(= "foobar" "foobar") ; => true (= 1 1.0) ; => false (== 1 1.0) ; => true (= '(1 2) [1 2]) ; => true (= {:a 1 :b 2} {:a 1 :b 2}) ; => true
Only nil and false are falsey, everything else is truthy
(defn truthy? [value] (if value true false)) (truthy? 0) ; => true (truthy? "") ; => true (truthy? []) ; => true (truthy? nil) ; => falseThe not-empty function is useful if you need empty data to evaluate to boolean false.
(if true "it is true" "it is false") ; => "it is true" (if (or (and "foo" "bar") "baz") "true" "false") ; => "true" (def n 5) (cond (< n 0) "is negative" (= n 0) "is zero" (> n 0) "is positive") ; => "is positive"there is also when and case. Note that and, or etc. are variadic in arity. Sometimes you need to use do to wrap multiple statements for example in an if clause.
(:foo nil) ; => nil (get nil :foo :default) ; => :default (first nil) ; => nil (next nil) ; => nil (inc nil) ; => NullPointerException
; File: app/util.clj (ns app.util) (def pi 3.14) (defn area [radius] (* pi (Math/pow radius 2)))
; File: app/web.clj (ns app.web (require [app.util :as util])) util/pi ; => 3.14 (util/area 2) ; => 12.56
(class 1.23) ; => java.lang.Double (ancestors (class 1.23)) ; => #{java.io.Serializable java.lang.Comparable ...} (instance? Double 1.23) ; => true (map #(% 1.23) [number? float? integer? zero? string? keyword? map? vector? nil?]) ; => (true true false false false false false false false) (clojure.reflect/reflect 1) ; => detailed info on class, ancestors, methods etc.
(defn map-invert [map] (reduce (fn [m [k v]] (assoc m v k)) {} map)) (map-invert {:a 1 :b 2}) ; => {1 :a, 2 :b} (def data {:a 1 :b 2}) (let [{:keys [a b c] :or {c 3}} data] (println a b c)) ; => 1 2 3 ; => nil
(defrecord User [name email admin]) (def joe (->User "Joe" "joe@example.com" false)) (def peter (map->User { :name "Peter" :email "peter@example.com" :admin true})) (class peter) ; => user.User
(defrecord User [^String name ^String email ^Boolean admin])
(defn len [x] (.length x)) (defn len2 [^String x] (.length x)) (time (reduce + (map len (repeat 1000000 "asdf")))) ; => "Elapsed time: 3007.198 msecs" (time (reduce + (map len2 (repeat 1000000 "asdf")))) ; => "Elapsed time: 308.045 msecs"
Example from TypeScript:
interface Named { name: string; } class Person { name: string; } let p: Named; p = new Person(); // OK, because of structural typingSee the TypeScript documentation for more details.
(require '[schema.core :as s]) (s/defrecord Ingredient [name :- s/Str quantity :- s/Int unit :- s/Keyword]) (s/defrecord Recipe [name :- s/Str ingredients :- [Ingredient]])
(require '[schema.core :as s]) (def flour (map->Ingredient {:name "Flour"})) (def pancakes (map->Recipe { :name "Pancakes" :ingredients [flour] })) (s/validate Recipe pancakes) ; => ExceptionInfo Value does not match schema...
(s/defn add-ingredients :- Recipe [recipe :- Recipe & ingredients :- [Ingredient]] (update-in recipe [:ingredients] into ingredients))
(defprotocol Lifecycle (start [component] "Synchronous, returns updated version of component") (stop [component] "Synchronous, returns updated version of component"))
(defrecord Application [database config] Lifecycle (start [component] (println "Starting Application...")) (stop [component] (println "Stopping Application...")))You can use extend-protocol to extend basic types with protocols. You can check if an object satisfies a protocol with satisfies?
(defprotocol Foo (foo [this])) (defprotocol Bar (bar [this])) (extend java.lang.Number Bar {:bar (fn [this] 42)}) (extend java.lang.String Foo {:foo (fn [this] "foo")} Bar {:bar (fn [this] "forty two")})
(satisfies? Foo "foobar") ; => true (foo "foobar") ; => "foo" (bar "foobar") ; => "forty two" (bar 123) ; => 42 (foo 123) ; => IllegalArgumentException No implementation of method
(require '[clojure.string :as str]) (def channels ["TV4" "TV4 totalt"]) (defn channel-key [channel] (-> channel (str/lower-case) (str/replace #" " "_") (keyword))) (map channel-key channels) ; => [:tv4 :tv4_totalt]For a discussion around handling errors in a function chain, see "Good Enough" error handling in Clojure
(def divisible? #(or (zero? (mod % 3)) (zero? (mod % 5)))) (->> (range 1000) (filter divisible?) (reduce +) println) (reduce + (filter divisible? (range 1000)))Euler problem 1 - the sum of all numbers under 1,000 that are divisible by either 3 or 5
(def counter (atom 0)) (swap! counter inc) (println @counter) ; => 1 (swap! counter inc) (println @counter) ; => 0Allows thread safe access to shared data. Example usage: memoize function. The available reference types in Clojure are Refs, Atoms, Agents, and Vars.
(def counter (atom 0)) (defn print-inc [value] (println (str "thread " (.getId (Thread/currentThread)) " value " (inc value))) (inc value)) (let [n 5] (future (dotimes [_ n] (swap! counter print-inc))) (future (dotimes [_ n] (swap! counter print-inc))) (future (dotimes [_ n] (swap! counter print-inc)))) @counter ; => 15
(def foo (ref 0)) (def bar (ref 1)) (alter foo inc) ; => IllegalStateException No transaction running (dosync (alter foo inc) (alter bar dec)) @foo ; => 1 @bar ; => 0
(def foo (agent 0)) (send foo inc) @foo ; => 1 (send foo #(/ % 0)) (send foo inc) ; => ArithmeticException Divide by zero
(defn error-handler [agent e] (println "Agent" agent " threw error " e)) (def foo (agent 0 :error-handler error-handler)) (send foo inc) @foo ; => 1 (send foo #(/ % 0)) ; => Agent ... threw error ... (send foo inc) @foo ; => 2
(require '[clojure.core.async :refer [chan >!! <:!!]]) (def foo (chan 10)) ; => 10 buffered channel (>!! foo 1) (<!! foo) ; => 1 (<!! foo) ; blocks...
(require '[clojure.core.async :refer [chan go alts! >!]]) (def foo (chan)) (def bar (chan)) (go (loop [] (let [v (alts! [foo bar])] (println "Received " v)) (recur))) (go (>! foo 1)) (go (>! bar 2))
(defn new-counter [] (let [count (atom 0)] #(swap! count inc))) (def counter (new-counter)) (take 10 (repeatedly counter)) ; => (1 2 3 4 5 6 7 8 9 10) (take 10 (iterate inc 0)) ; => (0 1 2 3 4 5 6 7 8 9) (take 10 (repeatedly rand)) ; => (0.9000394153557845 0.6840742826831324 ...)
(defmacro unless [arg & body] `(if (not ~arg) (do ~@body))) (unless false (println "body executing")) ; => body executing (macroexpand '(unless false (println "body executing"))) ; => (if (clojure.core/not false) ; (do (println "body executing")))A nice walk through of macros is available at Learn X in Y minutes and at Clojure for the Brave and True.
// From Apache Commons Lang, http://commons.apache.org/lang/ public static int indexOfAny(String str, char[] searchChars) { if (isEmpty(str) || ArrayUtils.isEmpty(searchChars)) { return -1; } for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); for (int j = 0; j < searchChars.length; j++) { if (searchChars[j] == ch) { return i; } } } return -1; }
(defn indexed [coll] (map-indexed vector coll)) (defn index-filter [pred coll] (for [[idx elt] (indexed coll) :when (pred elt)] idx)) (defn index-of-any [pred coll] (first (index-filter pred coll))) (index-of-any #{\b \y} "zzabyycdxx") ; => 3
(defn index-filter [pred coll] (let [index first value second] (->> (map-indexed vector coll) (filter (comp pred value)) (map index)))) (first (index-filter #{\b \y} "zzabyycdxx")) ; => 3
(spit "/tmp/foobar.txt" "here is some contents") (slurp "/tmp/foobar.txt")
(.toUpperCase "fred") ; => "FRED" (System/getenv "USER") ; => "peter.marklund" (Thread/sleep (rand-int 1000)) Math/PI ; => 3.141592... (doto (new java.util.HashMap) (.put "a" 1) (.put "b" 2)) ; => {"a" 1, "b" 2} (import [java.util Date]) (Date.) ; => #inst "2016-04-14T14:38:36.486-00:00"
(defn greet [name] (str "Hello " name)) (defn load-var [ns-name var-name] (require (symbol ns-name)) (ns-resolve (find-ns (symbol ns-name)) (symbol var-name))) ((load-var (ns-name *ns*) "greet") "Joe") ; => "Hello Joe"
Handlers are pure functions that take an HTTP request map and return an HTTP response map
(defn home [request] {:status 200 :headers {"Content-Type" "text/html"} :body "Hello World"})
:uri :headers :query-string :request-method :protocol :body ...
Middleware are handlers that wrap other handlers.
(defn wrap-response-time [handler] (fn [req] (time (handler req))))
(-> handler (wrap-keyword-params) (wrap-json-params {}) (wrap-params {}) (wrap-json-response {:pretty true}))These middleware together conviently put query params and JSON bodies in a params map in the request. The wrap-json-response middleware will serialize a data structure in the response body to JSON. In development you also want the wrap-reload middleware for reloading code.
(ns app.web (:require [ring.adapter.jetty :as jetty])) (defn app [request] {:status 200 :headers {"Content-Type" "text/html"} :body "Welcome to my Clojure API"}) (defn port [] (Integer. (or (System/getenv "PORT") 5000))) (defn -main [] (jetty/run-jetty app {:port (port) :join? false}))
(require '[clj-http.client :as client]) (client/get "https://api-endpoint" { :query-params {} :as :json}) (client/post "https://api-endpoint" { :form-params {:foo "bar"} :content-type :json :oauth-token "token"})))
(defn invoke-api-1 [] (Thread/sleep 1000) "api-1-data") (defn invoke-api-2 [] (Thread/sleep 1000) "api-2-data") (time (println (pcalls invoke-api-1 invoke-api-2))) ; => (api-data-1 api-data-2) ; => "Elapsed time: 1004.511257 msecs"
(def apis { :api1 invoke-api-1 :api2 invoke-api-2}) (defn pcalls-map [map] (zipmap (keys map) (apply pcalls (vals map)))) (time (println (pcalls-map apis))) ; => {:api1 api-data-1 :api2 api-data-2} ; => "Elapsed time: 1005.052411 msecs"
(require '[cheshire.core :as json]) (json/generate-string {:foo "bar"}) ; => {"foo":"bar"} (json/parse-string "{\"foo\":\"bar\"}" true) ; => {:foo "bar"}
There is a bare bones API example and a more extensive CMS REST API on Github.