Language reference
The language as of v0.60.0. Every example on this page was run against that toolchain; ; => marks the value it printed.
Literals
Comments start with ;. Commas are whitespace. There are no operators and no precedence: + and < are ordinary functions called in prefix position.
42 -3 0x1f 0o755 0b1011 1_000_000 ; integers
1.5 1.0e9 ; floats
1/3 ; an exact ratio
1.05M ; a decimal
"text with \"escapes\" and \n newlines" ; a string (a binary)
\a \space \newline ; characters are integers: 97 32 10
true false nil
:ok :not-found ; keywords
#(:ok 1420) ; a tuple
[1 2 3] ; a vector
{:name "Kaz" :age 30} ; a map
#{1 2 3} ; a set
'(1 2 3) ; a quoted list
#[65 66 67] ; a binary: "ABC"
~r"\d+" ; a regular expressionNumbers never overflow, division is exact, and floats and decimals stay distinct. Only false and nil are falsy.
(* 99999999999 99999999999) ; => 9999999999800000000001 (* (/ 1 3) 3) ; => 1 (+ 0.1 0.2) ; => 0.30000000000000004 (+ 0.1M 0.2M) ; => 0.3M (= 1 1.0) ; => false (if 0 "yes" "no") ; => "yes"
Bindings
def names a value in the namespace, evaluated once when the module loads; let binds locally. A name keeps its value — binding it again is a compile error, with a diagnostic code that praxis explain expands on.
(def bill 4260) (def- secret "A private constant." 42) (let [a 2 b 3] (+ a b)) ; => 5 (let [a 1 a 2] a) ; user.prax:1:1: PRX0207: a name keeps its value, and a already has one here ; -- use a different one
defonce keeps its value across a code reload. (const limit 10) declares a compile-time constant, read as @limit.
Functions
A definition reads: metadata, name, docstring, parameters, then optionally :spec [argument-types -> result-type] and a :when guard. A function may have several arities and several clauses; clauses match their parameters as patterns.
(defn greet "A greeting, with a default."
([] (greet "world"))
([who] (str "hello, " who)))
(greet) ; => "hello, world"
(greet "Kaz") ; => "hello, Kaz"
(defn fib
([n] :when (< n 2) n)
([n] (+ (fib (- n 1)) (fib (- n 2)))))
(fib 10) ; => 55
(defn total
([coll] (total coll 0))
([coll sum]
(if (seq/empty? coll)
sum
(recur (seq/rest coll) (+ sum (seq/first coll))))))
(total [3 1 4]) ; => 8
(defn sum-all [x & more] (seq/reduce more (fn [n acc] (+ n acc)) x))
(sum-all 1 2 3 4) ; => 10recur makes a flat loop on every machine; it must stand in tail position. defn- defines a private function.
Anonymous functions and threading
((fn [x] (* x x)) 7) ; => 49
(seq/map [1 2 3] &(* &1 2)) ; => (2 4 6)
(:name {:name "Kaz"}) ; => "Kaz"
(-> [1 2 3 4 5 6]
(seq/filter integer/even?)
(seq/map &(* &1 10))
(seq/reduce praxis.core/+ 0)) ; => 120
(some-> {:a {:b 5}} :a :b (+ 1)) ; => 6
(some-> {:a nil} :a :b) ; => nil
(cond-> 1 true (+ 1) false (* 100) true (* 10)) ; => 20&(…) is the short form of fn, with &1, &2 for its arguments. A keyword is a function that looks itself up in a map. Because every library function takes its subject first, -> covers nearly every pipeline; ->>, some->, cond-> and as-> exist for the rest.
Pattern matching
Patterns work in parameters, let, case, with and receive. A map pattern is written key first, the way a map is built. ?? gives a default for a missing key; = name binds the whole value. A binding that does not match fails with :no-match.
(let [#(:ok v) #(:ok 42)] v) ; => 42
(let [{:name n} {:name "Kaz" :age 30}] n) ; => "Kaz"
(let [{:age age ?? 0} {:name "Kaz"}] age) ; => 0
(defn first-two [[a b & more]] #(a b more))
(first-two [1 2 3 4]) ; => #(1 2 (3 4))
(defn tell ([#(:ok v)] v) ([#(:error why)] why))
(tell #(:error :locked)) ; => :locked
(case -4 n :when (< n 0) :negative 0 :zero :positive) ; => :negative
(defn direction
([~r"^(?<way>north|south)$"] way)
([_other] "nowhere"))
(seq/map ["north" "sideways"] direction) ; => ("north" "nowhere")
(with [#(:ok n) #(:ok 2)] (* n 10)) ; => 20
(with [#(:ok n) #(:error :nope)] (* n 10)) ; => #(:error :nope)A regular expression in a parameter list matches a string and binds its named groups. with runs its body only if every pattern matches, and otherwise returns the first value that did not. Binaries destructure by size and type, as in Erlang:
(defn frame [bin]
(let [#[[n 8] [body n :binary] [rest :binary]] bin]
#(n body rest)))
(frame #[3 65 66 67 68 69]) ; => #(3 "ABC" "DE")Control flow
(cond (< 4 3) :cold (< 4 25) :warm true :hot) ; => :warm (when (> 3 5) "yes") ; => nil (and false (/ 1 0)) ; => false (or :first (/ 1 0)) ; => :first (seq/into-list (for [x (seq/range 0 6) :when (integer/even? x)] (* x 10))) ; => (0 20 40) (seq/into-list (for [x [1 2] y [:a :b]] #(x y))) ; => (#(1 :a) #(1 :b) #(2 :a) #(2 :b)) (doseq [n [1 2 3]] (io/print-line (str "doseq " n)))
for is a lazy comprehension that takes :let, :when and :while; doseq is its eager twin for side effects.
Collections
Collections are persistent: an update returns a new value and leaves the old one untouched. Under the hood they are plain BEAM values — lists, maps, MapSets, binaries — so they cross into Erlang and Elixir without conversion.
(def stall {:name "the ramen stall" :open? true})
(map/assoc stall :seats 10)
; => {:name "the ramen stall", :open? true, :seats 10}
(map/keys {:a 1 :b 2}) ; => (:a :b)
(vector/conj [1 2] 3) ; => [1 2 3]
(set/union #{1 2} #{2 3}) ; => #{1 2 3}
(tuple/at #(:ok 1420) 1) ; => 1420
(seq/filter [3 1 4 1 5] (fn [n] (> n 2))) ; => (3 4 5)
(seq/sort-by ["pear" "fig" "banana"] string/length)
; => ("fig" "pear" "banana")
(seq/group-by [1 2 3 4 5] integer/even?) ; => {false (1 3 5), true (2 4)}
(seq/frequencies ["a" "b" "a"]) ; => {"a" 2, "b" 1}
(seq/zip [1 2 3] [:a :b :c]) ; => (#(1 :a) #(2 :b) #(3 :c))Subject first holds even where Clojure differs: (seq/cons (list 2 3) 1) is (1 2 3). A reducing function takes the item first and the accumulator second.
Records, protocols and types
(defrecord Point [x y]) ; positional: (->Point 1 2)
(defrecord Place {:name nil :exits {}}) ; with defaults: (map->Place {:name "alley"})
(:x (->Point 1 2)) ; => 1
(ns shapes "Anything with an area.")
(defprotocol Shape "Anything with an area." (area "How much `shape` covers." [shape]))
(defrecord Square [side])
(defimpl shapes Square (area [s] (* (:side s) (:side s))))
(shapes/area (->Square 3)) ; => 9
(deftype outcome "What a call comes to." #(:ok any) #(:error string))
(outcome? #(:ok 1)) ; => true
(outcome? :huh) ; => falseA record is an Elixir struct. A protocol lives in its own namespace — one protocol per namespace — and defimpl names that namespace; implementing the sequence protocol makes a record work with every seq/ function and with Elixir’s Enum. deftype defines a predicate name? from patterns, for specs and guards.
Namespaces and interop
Every file starts with (ns name "docstring" …). The standard library needs no require: string/, seq/, map/, io/ and the rest are available from the first line. Host modules come in the same way as Praxis ones:
(ns shop.core
"The shop."
(:require [shop.prices :as prices]
[(:erlang "lists") :as lists]
[(:elixir "String") :as estring]))
(lists/reverse (list 1 2 3)) ; => (3 2 1)
(estring/upcase "hello") ; => "HELLO"Definitions carry metadata — ^{:added "0.1.0"} and the like — which goes into the standard EEP-48 documentation chunk, so Erlang’s shell and ExDoc read Praxis documentation too. From Elixir, a Praxis namespace is a module: shop.core is :"praxis.shop.core".
Macros
(defmacro twice "`expr`, evaluated once and added to itself." [expr]
`(let [v# ~expr] (+ v# v#)))
(twice (+ 1 2)) ; => 6
`(foo ~(+ 1 2) ~@[4 5]) ; => (foo 3 4 5)
(defsigil w "Words." [body _modifiers]
(list 'quote (praxis.string/split body " ")))
~w"a b c" ; => ("a" "b" "c")Syntax-quote resolves symbols in the namespace that defines the macro, and a trailing # generates a fresh name, so macros are hygienic by default. defsigil defines a new ~x"…" literal; its expansion must be literal data.
Processes and OTP
spawn, send and receive are spelled the same on every machine: native processes on the BEAM, green processes on JavaScript. receive is selective, takes guards and an (after ms) timeout.
(defn counter [total]
(receive
#(:add n) (counter (+ total n))
#(:get from) (do (process/send from #(:total total)) (counter total))
:stop total))
(let [me (process/self)
pid (process/spawn (fn [] (counter 0)))]
(process/send pid #(:add 5))
(process/send pid #(:add 37))
(process/send pid #(:get me))
(receive #(:total t) t (after 1000) :timeout)) ; => 42OTP is there in Praxis terms: defbehaviour declares a behaviour and (:behaviour …) in the ns adopts one; praxis.actor wraps gen_server, praxis.supervisor builds supervision trees. A compiled project is an OTP application, and prx release builds a release. Supervision also runs on node. See the supervised actor example.
Errors
A failure is a name and a map. throw raises one; a single catch clause binds both; finally always runs and rethrow passes the failure on. Errors from the host are translated into the same shape.
(defn safe-div [a b]
(try #(:ok (/ a b))
(catch failure data #(:error failure (:message data)))))
(safe-div 10 4) ; => #(:ok 5/2)
(safe-div 10 0) ; => #(:error :division-by-zero "division by zero")
(defn order [item]
(try (throw :out-of-stock {:item item})
(catch failure data
(case failure
:out-of-stock (str "sorry, no " (:item data))
_ (rethrow)))
(finally (io/print-line "(finally ran)"))))
(order "ramen") ; prints (finally ran), => "sorry, no ramen"For values that may fail, #(:ok …) and #(:error …) with praxis.result are the idiom: (result/collect [#(:ok 1) #(:error :a) #(:error :b)]) gives #(:error (:a :b)) — every error, not just the first. Named failures with fields are declared with deferror in a <base>.errors namespace.
Compiler diagnostics carry codes and point at the source:
user.prax:1:6: PRX0116: nothing defines totl -- not a binding here, not this
namespace's, and not referred from another one
1 | (+ 1 totl)
| ^^^^Strings
A string is a UTF-8 binary. There is no string interpolation yet — it is on the list of ideas, not the plan. Strings are built with str and the string/ namespace:
(str "total: " 1/3 " " :k) ; => "total: 1/3 k" (string/join ["a" "b"] ", ") ; => "a, b" (string/length "night market") ; => 12 (re/matches? "seat 8" ~r"\d+") ; => true
Standard library
Every definition in the standard library has a docstring and a run example, and nearly all have a :spec.
| Area | Namespaces |
|---|---|
| Data | seq stream collect map set vector tuple keyword queue walk result spec |
| Numbers | integer float decimal math bits random |
| Text and bytes | string re bytes codec json uri crypto ansi pxn |
| System | io file path time system code app logger cli |
| Concurrency | process actor supervisor atom kv pool cache node |
| Network | net.tcp net.udp net.tls net.socket net.dns — deliberately no HTTP client |
| Browser | dom event storage history location service-worker |
Projects
A project is described by project.pxn, which is Praxis data. prx new shop --app writes this, together with sources, tests and a README:
(project shop
{:license :apache-2
:version "0.1.0"
:main shop.app
:description "TODO: describe your absolutely amazing project here."
:deps [[prx_fortune :hex "~> 0.18" {:only [:dev]}]]})Dependencies come from Hex, git, a path or the same workspace: [jason :hex "~> 1.4"], [thing :git "https://…" {:tag "v1.0"}], [local :path "../local"]. A manifest can also choose a layout — a single project, a workspace of packages, or a suite of apps and libraries. Runtime configuration is CASC.
prx new shop --app # a new application prx run # compile and run it prx test # run the tests prx compile --target js # compile for JavaScript prx release # build an OTP release prx --ai … # any command, answering in JSON