joetjen.net
EN DE
Programming language

Language reference

The language as of v0.63.0. Every example on this page was run against that toolchain; ; => marks the value it printed.

In development · v0.63

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
:"worker@host"                              ; a keyword with any name
#(: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"
~c"ab"                                      ; a charlist: Erlang’s kind of string
~r"\d+"                                     ; a regular expression

Numbers 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"

A keyword in quotes takes any name, so :"worker@host" — a node name, or a keyword read from a DXN document — can be written where a bare keyword cannot. A charlist is what Erlang hands back for a filename, a hostname or an error reason; ~c"…" writes one, and one of printable characters prints that way.

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
src/shop/core.prax
(ns shop.core "The shop.")

(defn split [] (let [a 1 a 2] a))
prx: src/shop/core.prax:3:16: PRX0208: 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)  ; => 10

(def double "Doubles x." (fn [x] (* x 2)))   ; the same as defn
(double 21)      ; => 42

recur makes a flat loop on every machine; it must stand in tail position. def with an fn is the primitive way to define a function, and defn is the same thing said shorter. A trailing dash — defn-, def-, defmacro- — makes a definition private; it writes ^{:private true}, the one thing that decides it.

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
(case ["posts" 7] ["users" id] id _ :other)   ; => :other

(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}
(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. A map has no order, so map/keys answers its keys in whichever order the map holds them.

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)                               ; => false

A record is an Elixir struct, and keeps its capital as a type does; every other name in Praxis code is lowercase. 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". A definition may not take a name praxis.core already has (PRX0128) unless the file says so with (refer-praxis :exclude [name]).

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 and takes a docstring, a :spec and a body of any length; 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))   ; => 42

OTP 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 …) are the idiom, and praxis.result works with them rather than matching on them — for the cases with covers badly: every error rather than the first, a collection of results, and both halves of a batch that partly failed. Named failures with fields are declared with deferror in a <base>.errors namespace.

(result/collect [#(:ok 1) #(:error :a) #(:error :b)])   ; => #(:error (:a :b))
(result/partition [#(:ok 1) #(:error :a) #(:ok 2)])     ; => #((1 2) (:a))
(result/then #(:ok 2) (fn [n] #(:ok (* n 10))))         ; => #(:ok 20)
(result/value-or #(:error :x) 0)                        ; => 0
(result/from-nil nil :missing)                          ; => #(:error :missing)

Compiler diagnostics carry codes and point at the source:

src/shop/core.prax
(ns shop.core "The shop.")

(defn total [] (+ 1 totl))
prx: src/shop/core.prax:3:21: PRX0116: nothing defines totl -- not a binding here, not this namespace's, and not referred from another one

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
(list 65 66)                                  ; => ~c"AB"

The last line is the ambiguity Erlang has always had, now visible: a list of printable character codes prints as the charlist it may stand for, because nothing in the value says which was meant.

Standard library

Every definition in the standard library has a docstring and an example, and nearly all have a :spec. The examples are run as tests in the gate; one whose result depends on the machine, such as a socket’s, is marked as shown only for its shape.

AreaNamespaces
Dataseq stream collect map set vector tuple keyword queue walk result spec function
Numbersinteger float decimal math bits random
Text and bytesstring inspect re bytes codec json uri crypto ansi pxn
Systemio file path time system code app logger
Concurrencyprocess actor supervisor atom kv pool cache node
Networknet.tcp net.udp net.tls net.socket net.dns — deliberately no HTTP client
Browserdom event storage history location service-worker

praxis.time holds four kinds of time — an instant, a date, a clock time and a local date and time — and instant?, date?, clock? and local? say which one a value is.

Projects

A project is described by project.pxn, which is Praxis data. prx new shop --app writes this, together with sources, tests, a README, a changelog and a licence:

project.pxn
(project shop

  ; "Premature optimization is the root of all evil."
  ;     -- Donald Knuth

  {:license :apache-2
   :version "0.1.0"
   :main shop.app
   :description "TODO: describe your absolutely amazing project here."
   :deps [[touchstone :hex "~> 0.18" {:only [:test]}]
          [touchstone_prx :hex "~> 0.18" {:only [:dev :test]}]
          [stet_prx :hex "~> 0.18" {:only [:dev]}]
          [prx_fortune :hex "~> 0.18" {:only [:dev]}]]})

The test framework, its prx test plugin and the formatter are declared from the start. 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