Skip to main content

Cheat Sheet

On this page

Quick reference for Phel syntax and core functions.

AI agents: load Agentic Coding first for the truncation-safe rules and PHP-interop gotchas. This sheet is the wide surface; that one is the must-know.

Basic syntax#

;; This is a standalone comment
; inline comment (after an expression)

nil                     ; null value
true false              ; booleans (only false and nil are falsy)
42 -3 1.5 3.14e2        ; numbers
0xFF 0b1010 017         ; hex, binary, octal
"hello" "line\nbreak"   ; strings
:keyword :status        ; keywords (interned constants)
my-var my-module/fn     ; symbols
#"[a-z]+"               ; regex literal (PCRE pattern)

Note: # line and #| |# multiline comments are deprecated. Use ;; for standalone comments and ; for inline comments.

See Basic Types.

Reader syntax#

@my-var                 ; shorthand for (deref my-var)
#"pattern"              ; regex literal (PCRE)
#(+ %1 %2)             ; anonymous function shorthand
#(inc %)                ; single-arg: % is the same as %1
#(apply + %&)           ; variadic: %& captures rest args
#?(:phel expr1 :default expr2)  ; reader conditional
#?@(:phel [a b] :default [c])  ; splicing reader conditional

;; Tagged literals
#inst "2026-01-15T12:00:00Z"    ; => DateTimeImmutable
#uuid "550e8400-e29b-41d4-a716-446655440000"  ; => UUID string
#regex "\\d+"                    ; => PCRE pattern string

;; First-class var handles
#'my-fn                 ; shorthand for (var my-fn)
(var my-fn)             ; returns the Var object for my-fn

#(...) is the preferred shorthand. % or %1 first arg, %2 second, %& rest. Legacy |(...) with $ is deprecated.

Reader conditionals (#?(), #?@()) target platforms in .cljc via :phel and :default.

Tagged literals: #inst reads as DateTimeImmutable, #uuid as a UUID string, #regex as a PCRE pattern. Register custom tags with register-tag from phel.reader.

Data structures#

[1 2 3]                 ; vector (indexed)
(vector 1 2 3)          ; same thing
{:a 1 :b 2}             ; map (key-value pairs)
(hash-map :a 1 :b 2)    ; same thing
#{1 2 3}                ; set (unique values)
(hash-set 1 2 3)        ; set from arguments
(set [1 2 3])           ; coerce collection to set
'(1 2 3)                ; quoted list (data, not a call)
(list 1 2 3)            ; same thing

See Data Structures.

Accessing data#

(get [1 2 3] 0)           ; => 1
(get {:a 1} :a)            ; => 1
(get {:a 1} :b "default")  ; => "default"
(get-in {:a {:b 1}} [:a :b])  ; => 1
(first [1 2 3])            ; => 1
(second [1 2 3])           ; => 2
(peek [1 2 3])             ; => 3
(:name {:name "Alice"})    ; => "Alice" (keyword as function)
({:a 1 :b 2} :a)           ; => 1 (map as function)
([10 20 30] 1)             ; => 20 (vector as function)

Modifying data#

(conj [1 2] 3)                    ; => [1 2 3]
(conj #{1 2} 3)                   ; => #{1 2 3}
(conj {:a 1} [:b 2])              ; => {:a 1, :b 2}
(assoc {:a 1} :b 2)               ; => {:a 1, :b 2}
(assoc [1 2 3] 0 9)               ; => [9 2 3]
(dissoc {:a 1 :b 2} :a)           ; => {:b 2}
(update {:a 1} :a inc)            ; => {:a 2}
(update-keys {:a 1 :b 2} name)    ; => {"a" 1, "b" 2}
(update-vals {:a 1 :b 2} inc)     ; => {:a 2, :b 3}
(assoc-in {} [:a :b] 1)           ; => {:a {:b 1}}
(update-in {:a {:b 1}} [:a :b] inc)  ; => {:a {:b 2}}
(merge {:a 1} {:b 2 :a 3})        ; => {:a 3, :b 2}

See Data Structures.

Destructuring#

;; Sequential destructuring
(let [[a b c] [1 2 3]]
  (+ a b c))                      ; => 6

(let [[a b & rest] [1 2 3 4 5]]
  rest)                            ; => [3 4 5]

;; Associative destructuring
(let [{:name name :age age} {:name "Alice" :age 30}]
  (str name " is " age))          ; => "Alice is 30"

;; Default values
(let [{:name name :role role :or {role "guest"}}
      {:name "Bob"}]
  role)                            ; => "guest"

;; Works in defn, fn, loop too
(defn greet [{:name name}]
  (str "Hello, " name))
(greet {:name "Alice"})            ; => "Hello, Alice"

See Destructuring.

Defining things#

(def pi 3.14159)                  ; global binding
(def secret :private 42)          ; private binding
(defonce conn (connect!))         ; bind once; skipped if already defined (survives REPL reloads)

(defn greet [name]                ; public function
  (str "Hello, " name))

(defn- helper [x]                 ; private function
  (* x 2))

(defstruct point [x y])           ; struct (typed map)
(point 1 2)                       ; => (point 1 2)
(point? (point 1 2))              ; => true

(let [x 1                         ; local bindings
      y (+ x 2)]
  (+ x y))                        ; => 4

(defmulti area :shape)             ; multimethod (dispatch on :shape)
(defmethod area :circle [{:radius r}]
  (* 3.14 r r))

See Global and Local Bindings.

Functions#

(fn [x] (* x 2))                  ; anonymous function
#(* % 2)                           ; short form (single param)
#(+ %1 %2)                        ; short form (multiple params)
#(apply + %&)                     ; short form (variadic)

(defn greet                        ; multi-arity
  ([] "Hi")
  ([name] (str "Hi " name)))

(defn sum [& nums]                 ; variadic
  (apply + nums))

(apply + [1 2 3])                  ; => 6
(partial + 10)                     ; => fn that adds 10
(comp inc inc)                     ; => fn that increments twice
(identity 42)                      ; => 42
(memoize expensive-fn)             ; => cached version of fn
(memoize-lru expensive-fn 100)     ; => cached with max 100 entries

(defn ^:memoize fib [n] ...)       ; defn metadata: auto-wraps in memoize
(defn ^{:memoize-lru 128} f [k] ...)
(defn ^:async fetch [url] ...)     ; wraps body in (async ...) -> Amp\Future

(defn ^int add [^int a ^int b]     ; :tag metadata -> PHP type decls
  (+ a b))

See Functions and Recursion.

Control flow#

(if (> x 0) "pos" "non-pos")      ; if/else
(when (> x 0) (print "pos"))      ; when (no else branch)

(cond
  (< n 0) "negative"
  (= n 0) "zero"
  :else "positive")

(case status
  200 "OK"
  404 "Not Found")

(do (print "a") (print "b") 42)   ; evaluate multiple exprs, return last

See Control Flow.

Loops & recursion#

(loop [acc 0 n 10]                 ; loop with recur
  (if (= n 0)
    acc
    (recur (+ acc n) (dec n))))    ; => 55

(foreach [v [1 2 3]]              ; side-effects only, returns nil
  (print v))

(for [x :in [1 2 3]] (* x 2))    ; => [2 4 6] (list comprehension)
(for [x :range [0 5]] x)          ; => [0 1 2 3 4]
(for [x :in [1 2 3 4]
      :when (even? x)] x)         ; => [2 4]

(dotimes [i 3] (print i))         ; prints 0, 1, 2

See Functions and Recursion, Control Flow.

Collections#

(def users [{:role :admin} {:role :user} {:role :admin}])

(map inc [1 2 3])                  ; => @[2 3 4]
(filter even? [1 2 3 4])          ; => @[2 4]
(reduce + 0 [1 2 3])              ; => 6
(sort [3 1 2])                    ; => [1 2 3]
(sort-by :age [{:age 30} {:age 20}])  ; sort by key
(group-by :role users)             ; map of role -> [users]
(frequencies [:a :b :a :a])        ; => {:a 3, :b 1}
(count [1 2 3])                    ; => 3
(empty? [])                        ; => true
(contains? {:a 1} :a)             ; => true
(some even? [1 3 4])              ; => true
(every? pos? [1 2 3])             ; => true
(into #{} [1 2 1 3])              ; => #{1 2 3}
(vec '(1 2 3))                     ; => [1 2 3] (coerce to vector)
(subset? #{1 2} #{1 2 3})         ; => true
(superset? #{1 2 3} #{1 2})       ; => true
(distinct [1 2 1 3 2])            ; => @[1 2 3]
(flatten [[1 2] [3 [4]]])         ; => @[1 2 3 4]
(reverse [1 2 3])                  ; => [3 2 1]
(concat [1 2] [3 4])              ; => @[1 2 3 4]
(compact [1 nil 2 nil 3])         ; => @[1 2 3]
(remove neg? [1 -2 3 -4])        ; => @[1 3]

See Data Structures.

Walking data structures#

Requires (:require phel.walk :refer [postwalk prewalk postwalk-replace keywordize-keys stringify-keys]).

(postwalk f nested)                ; transform bottom-up
(prewalk f nested)                 ; transform top-down
(postwalk-replace {:a :x} [:a :b]) ; => [:x :b]
(keywordize-keys {"name" "Alice"}) ; => {:name "Alice"}
(stringify-keys {:name "Alice"})   ; => {"name" "Alice"}

See Data Structures.

Lazy sequences#

(take 5 (range))                   ; => @[0 1 2 3 4]
(take 5 (iterate inc 0))          ; => @[0 1 2 3 4]
(take 7 (cycle [1 2 3]))          ; => @[1 2 3 1 2 3 1]
(take 4 (repeat :x))              ; => @[:x :x :x :x]
(take 5 (repeatedly #(php/rand 1 100)))  ; 5 random numbers

(drop 3 (range 10))               ; => @[3 4 5 6 7 8 9]
(take-while pos? [3 2 1 0 -1])   ; => @[3 2 1]
(drop-while pos? [3 2 1 0 -1])   ; => @[0 -1]
(partition 2 [1 2 3 4 5 6])       ; => @[[1 2] [3 4] [5 6]]
(interleave [:a :b :c] [1 2 3])  ; => @[:a 1 :b 2 :c 3]

;; Lazy filtering + transformation
(->> (range)
     (filter even?)
     (take 5))                     ; => @[0 2 4 6 8]

;; Custom lazy sequence
(defn fibs []
  (lazy-seq (cons 0 (cons 1
    (map + (fibs) (rest (fibs)))))))

(doall (take 8 (fibs)))           ; => [0 1 1 2 3 5 8 13]
(dorun (map println [1 2 3]))     ; => nil (realize for side effects only)
(realized? (lazy-seq [1 2 3]))    ; => false

Lazy file I/O:

(line-seq (php/fopen "file.txt" "r"))  ; lazy line-by-line reading
(file-seq "src/")                       ; lazy recursive directory listing
(csv-seq (php/fopen "data.csv" "r"))   ; lazy CSV parsing
(read-file-lazy "big.txt" 4096)        ; lazy chunked reading

map, filter, take, drop, concat, mapcat, interleave, partition return lazy sequences.

Threading macros#

(-> {:name "Alice" :age 30}        ; thread-first
    (assoc :role "admin")
    (dissoc :age))                  ; => {:name "Alice", :role "admin"}

(->> [1 2 3 4 5]                   ; thread-last
     (filter odd?)
     (map inc))                    ; => @[2 4 6]

(as-> [1 2 3] v                    ; thread with named binding
      (conj v 4)
      (count v))                   ; => 4

(cond-> 1                          ; conditional thread-first
        true inc
        false (* 42))              ; => 2

(cond->> [1 2 3]                   ; conditional thread-last
         true (map inc)
         false (filter odd?))      ; => @[2 3 4]

Strings#

(str "Hello" " " "World")         ; => "Hello World"
(str "n=" 42)                      ; => "n=42"
(format "Hi %s, age %d" "Jo" 25)  ; => "Hi Jo, age 25"

Requires (:require phel.string :as str):

(ns my-app.strings
  (:require phel.string :as str))

(str/lower-case "HELLO")           ; => "hello"
(str/upper-case "hello")           ; => "HELLO"
(str/replace "foo" "o" "0")        ; => "f00"
(str/subs "hello" 1 3)             ; => "el"
(str/split "a,b,c" #",")          ; => ["a" "b" "c"] (Phel vector)
(str/join ", " ["a" "b" "c"])      ; => "a, b, c"
(str/starts-with? "hello" "he")   ; => true
(str/ends-with? "hello" "lo")     ; => true
(str/trim "  hi  ")               ; => "hi"
(str/capitalize "hello world")    ; => "Hello world"
(str/reverse "hello")             ; => "olleh"

Regular expressions#

;; Regex literals use #"..." syntax (PCRE patterns)
(re-find #"\d+" "abc123def")       ; => "123"
(re-find #"(\w+)@(\w+)" "user@host")
                                   ; => ["user@host" "user" "host"]
(re-matches #"\d+" "123")          ; => "123"
(re-matches #"\d+" "abc123")       ; => nil (must match entire string)

;; re-seq: all matches as a vector
(re-seq #"\d+" "a1b2c3")          ; => ["1" "2" "3"]

;; Use regex for validation
(defn valid-email? [s]
  (some? (re-matches #".+@.+\..+" s)))

(valid-email? "alice@example.com") ; => true
(valid-email? "not-an-email")      ; => false

Mutable state#

(def counter (atom 0))             ; create an atom (mutable container)
(deref counter)                    ; => 0
@counter                           ; => 0 (shorthand for deref)
(reset! counter 42)                ; direct reset
@counter                           ; => 42
(swap! counter inc)                ; apply function, counter is now 43
(swap! counter + 10)               ; counter is now 53

;; Watchers: react to state changes
(add-watch counter :logger
  (fn [key ref old-val new-val]
    (println (str "Changed from " old-val " to " new-val))))
(remove-watch counter :logger)

;; Validators: constrain allowed values
(set-validator! counter #(>= % 0))  ; only non-negative values
(get-validator counter)             ; => the validator fn

See Global and Local Bindings.

Error handling#

(try
  (/ 1 0)
  (catch DivisionByZeroError e
    (str "Error: " (.getMessage e))))

(try
  (do-risky-thing)
  (catch Exception e
    (println (str "Failed: " (.getMessage e))))
  (finally
    (cleanup)))

(throw (InvalidArgumentException. "bad input"))

;; Structured exceptions with ex-info
(throw (ex-info "User not found" {:id 42 :type :not-found}))

(try
  (throw (ex-info "Validation failed" {:field :email} nil))
  (catch Exception e
    (ex-message e)                 ; => "Validation failed"
    (ex-data e)                    ; => {:field :email}
    (ex-cause e)))                 ; => nil

See PHP Interop.

Interfaces & structs#

(definterface Greetable
  (greet [this]))

(definterface HasArea
  (area [this]))

(defstruct circle [radius]
  HasArea
  (area [this] (* 3.14159 radius radius)))

(defstruct person [name age]
  Greetable
  (greet [this] (str "Hello, I'm " name)))

(greet (person "Alice" 30))        ; => "Hello, I'm Alice"
(area (circle 5))                  ; => 78.53975
(person? (person "Alice" 30))      ; => true

See Interfaces.

Protocols#

Polymorphic dispatch on the first argument's type. More flexible than interfaces, extendable to existing types.

;; Define a protocol
(defprotocol Stringable
  (to-string [this]))

(defstruct dog [name breed])

(extend-type dog
  Stringable
  (to-string [this] (str (:name this) " the " (:breed this))))

(to-string (dog "Rex" "Labrador")) ; => "Rex the Labrador"

;; Extend multiple types at once with extend-protocol
(extend-protocol Stringable
  :string  (to-string [this] this)
  :int     (to-string [this] (str this)))

;; Check protocol support
(satisfies? Stringable (dog "Rex" "Labrador"))  ; => true
(extends? Stringable :string)                    ; => true
(extends? Stringable :array)                     ; => false

Hierarchy system#

Ad-hoc hierarchies for multimethods and isa?.

(derive :shape/square :shape/poly)
(derive :shape/circle :shape/poly)
(derive :shape/filled-square :shape/square)

(isa? :shape/square :shape/poly)         ; => true
(isa? :shape/filled-square :shape/poly)  ; => true
(parents :shape/square)                  ; => #{:shape/poly}
(ancestors :shape/filled-square)         ; => #{:shape/square :shape/poly}
(descendants :shape/poly)                ; => #{:shape/square :shape/circle :shape/filled-square}

(make-hierarchy)                   ; => {:parents {}, :descendants {}, :ancestors {}}

Transducers#

Composable transformations independent of the data source. Avoid intermediate collections.

;; Basic transducer usage with transduce
(transduce (map inc) + 0 [1 2 3])       ; => 9
(transduce (filter even?) + 0 [1 2 3 4]) ; => 6

;; Compose transducers (left-to-right order)
(def xf (comp (filter even?) (map inc)))
(transduce xf conj [] [1 2 3 4 5 6])    ; => [3 5 7]

;; into with a transducer (3-arg form)
(into [] (map inc) [1 2 3])              ; => [2 3 4]
(into #{} (filter odd?) [1 2 3 2 1])     ; => #{1 3}

;; sequence: lazy transducer application
(sequence (map inc) [1 2 3])             ; => [2 3 4]

;; cat: concatenating transducer for nested collections
(into [] cat [[1 2] [3 4] [5]])          ; => [1 2 3 4 5]

;; completing: supply a final step to a reducing function
(transduce (map inc) (completing + str) 0 [1 2 3])  ; => 9

;; Many core fns have transducer arities (called with no collection):
;; (map f), (filter pred), (take n), (drop n), (partition-all n), etc.

PHP interop#

;; Calling PHP functions
(php/strlen "test")                ; => 4
(php/date "Y-m-d")                 ; => "2026-02-07"
(php/array_merge arr1 arr2)        ; call any PHP function

;; Instantiation - all three forms are equivalent
(php/new DateTime "now")
(new DateTime "now")
(DateTime. "now")                  ; ClassName. shorthand (preferred)

;; Instance methods & properties
(php/-> obj (method arg))          ; $obj->method($arg)
(php/-> obj property)              ; $obj->property
(php/-> obj (a) (b) (c))           ; chained: $obj->a()->b()->c()
(.method obj arg)                  ; shorthand
(.-property obj)                   ; property shorthand

;; Static methods & properties
(php/:: MyClass CONST)             ; MyClass::CONST
(php/:: MyClass (create "x"))      ; MyClass::create("x")
(MyClass/create "x")               ; static shorthand
Ns.MyClass/CONST                   ; static member shorthand

;; PHP arrays
(php/aget arr 0)                   ; $arr[0] ?? null
(php/aset arr "k" "v")             ; $arr["k"] = "v"
(php/apush arr "v")                ; $arr[] = "v"

See PHP Interop.

Namespaces#

(ns my-app.handlers
  (:require my-app.db)              ; import Phel module
  (:require my-app.utils :as u)     ; with alias
  (:require my-app.auth :refer [login logout])  ; import symbols
  (:use DateTimeImmutable)           ; import PHP class
  (:use Some.Long.Name :as Short))  ; PHP class with alias

(db/query "SELECT 1")               ; use module prefix
(u/format-date date)                 ; use alias
(login credentials)                  ; use referred symbol
(DateTimeImmutable.)                 ; use imported class (ClassName. shorthand)

See Namespaces.

Testing#

(ns my-app.tests
  (:require phel.test :refer [deftest is are]))

(deftest addition-test
  (is (= 4 (+ 2 2)))
  (is (= 4 (+ 2 2)) "optional description"))

(deftest multiple-assertions
  (are [expected input] (= expected (inc input))
    2 1
    3 2
    4 3))

(deftest exception-test
  (is (thrown? Exception
    (throw (php/new Exception "boom")))))
./vendor/bin/phel test                       # run all tests
./vendor/bin/phel test tests/main.phel       # run specific file
./vendor/bin/phel test --filter my-test      # filter by name
./vendor/bin/phel test --fail-fast           # stop on first failure

See Testing.

Async & concurrency#

async, await, await-all, await-any, ->closure are in phel.core (AMPHP-backed fibers).

;; Run body in a new fiber, returns an Amp\Future
(def f (async (+ 1 2)))
(await f)                          ; => 3 (blocks until resolved)

;; Await multiple futures concurrently
(await-all [(async 1) (async 2)]) ; => [1 2]
(await-any [(async 1) (async 2)]) ; => 1 (first to resolve)

;; Convert Phel fn to PHP Closure (for AMPHP and other libraries)
(->closure (fn [x] (* x 2)))

;; pmap: parallel map via fibers
(pmap inc [1 2 3])                 ; => [2 3 4]

Delay & force#

delay, delay?, and force are in phel.core (auto-imported, no require needed). phel.async/delay is a different function that suspends a fiber for N seconds.

;; Delay defers evaluation until first access
(def d (delay (do (println "computing...") 42)))
(delay? d)                         ; => true
(force d)                          ; prints "computing...", => 42
(force d)                          ; => 42 (cached, no recomputation)

Iteration#

;; iteration: produce a lazy sequence from a step function
;; Useful for paginated APIs or stateful producers
(defn fetch-page [token]
  {:items [1 2 3] :next-token (when (nil? token) "page2")})

(iteration fetch-page
  {:kf :next-token
   :vf :items
   :initk nil})

Arithmetic#

(+ 1 2 3)                          ; => 6
(- 10 3)                           ; => 7
(* 2 3 4)                          ; => 24
(/ 10 2)                           ; => 5
(/ 10 3)                           ; => 10/3 (Ratio, exact)
(/ 10.0 3)                         ; => 3.333... (float)
(float (/ 10 3))                   ; => 3.333... (coerce Ratio to float)
(quot 10 3)                        ; => 3 (integer quotient)
(rem 10 3)                         ; => 1 (remainder)
(mod -10 3)                        ; => 2 (modulo, always non-negative)
(** 2 10)                          ; => 1024

Integer division (/) returns a Ratio when not evenly divisible. Use float or (/ 10.0 3) if you need a float.

Utility functions#

(parse-long "42")                  ; => 42
(parse-double "3.14")              ; => 3.14
(parse-boolean "true")             ; => true
(abs -5)                           ; => 5
(inf? php/INF)                     ; => true
(nan? (php/log -1))                ; => true
(random-uuid)                      ; => "550e8400-e29b-..." (random UUID string)

Serialization (EDN & Transit)#

(ns my-app.serialize
  (:require phel.edn :as edn)
  (:require phel.transit :as transit))

;; phel.edn: eval-free EDN read/write (data only, no code execution)
(edn/read-string "{:a 1 :b [2 3]}")    ; => {:a 1, :b [2 3]}
(edn/write-string {:a 1 :b [2 3]})     ; => "{:a 1, :b [2 3]}"
(edn/read-string-all "1 2 3")          ; => [1 2 3] (every top-level form)

;; phel.transit: Transit + JSON-Verbose read/write
(transit/write-string {:a 1})          ; => "[\"~#cmap\",[\"~:a\",1]]"
(transit/read-string "[\"~:foo\",1]")  ; => [:foo 1]

Reflection#

(ns my-app.introspect
  (:require phel.reflect :as reflect))

;; phel.reflect: introspect PHP classes via reflection
(reflect/class-info \DateTime)         ; => map of name, methods, properties, ...
(reflect/methods \DateInterval)        ; => vector of method-info maps
(reflect/properties \DateInterval)     ; => vector of property-info maps
(reflect/supers \RuntimeException)     ; => parent classes + interfaces

REPL utilities#

(source my-fn)                     ; print source code of a function
(phel.repl/find-fn "map")          ; search for functions by name
(symbol-info 'map)                 ; detailed info about a symbol
(ns-publics 'phel.core)           ; all public vars in a namespace
(ns-aliases 'my-app.core)         ; namespace aliases
(ns-refers 'my-app.core)          ; referred symbols
(ns-list)                          ; list all loaded namespaces
(macroexpand-1 '(when true 1))    ; expand one level of macro
(macroexpand '(when true 1))      ; fully expand macro
(eval-str "(+ 1 2)")              ; evaluate a string of Phel code
(load-file "src/my-module.phel")  ; load and evaluate a file
(test-ns "my-app.tests")          ; run tests in a namespace (name as string)

Next steps#