Sprachreferenz
Die Sprache mit Stand v0.63.0. Jedes Beispiel auf dieser Seite lief gegen diese Toolchain; ; => markiert den ausgegebenen Wert.
Literale
Kommentare beginnen mit ;. Kommas sind Leerraum. Es gibt keine Operatoren und keine Rangfolge: + und < sind gewöhnliche Funktionen in Präfix-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 expressionZahlen laufen nie über, Division ist exakt, und Gleitkomma- und Dezimalzahlen bleiben getrennt. Nur false und nil sind falsch.
(* 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"
Ein Keyword in Anführungszeichen nimmt jeden Namen, so lässt sich :"worker@host" — ein Knotenname oder ein aus einem DXN-Dokument gelesenes Keyword — schreiben, wo ein bloßes Keyword es nicht kann. Eine Charlist ist das, was Erlang für einen Dateinamen, einen Hostnamen oder einen Fehlergrund zurückgibt; ~c"…" schreibt eine, und eine aus druckbaren Zeichen wird auch so ausgegeben.
Bindungen
def benennt einen Wert im Namespace, einmal beim Laden des Moduls ausgewertet; let bindet lokal. Ein Name behält seinen Wert — ihn erneut zu binden ist ein Compilerfehler, mit einem Diagnosecode, den praxis explain erläutert.
(def bill 4260) (def- secret "A private constant." 42) (let [a 2 b 3] (+ a b)) ; => 5
(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 behält seinen Wert über ein Code-Reload hinweg. (const limit 10) deklariert eine Konstante zur Compile-Zeit, gelesen als @limit.
Funktionen
Eine Definition liest sich so: Metadaten, Name, Docstring, Parameter, dann optional :spec [argument-typen -> ergebnis-typ] und ein :when-Guard. Eine Funktion kann mehrere Stelligkeiten und mehrere Klauseln haben; Klauseln matchen ihre Parameter als Muster.
(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) ; => 42recur erzeugt auf jeder Maschine eine flache Schleife; es muss in Endposition stehen. def mit einem fn ist die primitive Art, eine Funktion zu definieren, und defn sagt dasselbe kürzer. Ein angehängter Bindestrich — defn-, def-, defmacro- — macht eine Definition privat; er schreibt ^{:private true}, und nur das entscheidet darüber.
Anonyme Funktionen und 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&(…) ist die Kurzform von fn, mit &1, &2 für die Argumente. Ein Keyword ist eine Funktion, die sich selbst in einer Map nachschlägt. Weil jede Bibliotheksfunktion ihr Subjekt zuerst nimmt, deckt -> fast jede Pipeline ab; für den Rest gibt es ->>, some->, cond-> und as->.
Pattern Matching
Muster funktionieren in Parametern, let, case, with und receive. Ein Map-Muster wird Schlüssel zuerst geschrieben, so wie eine Map gebaut wird. ?? liefert einen Default für einen fehlenden Schlüssel; = name bindet den ganzen Wert. Eine Bindung, die nicht passt, scheitert mit :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)Ein regulärer Ausdruck in einer Parameterliste matcht eine Zeichenkette und bindet ihre benannten Gruppen. with führt seinen Rumpf nur aus, wenn jedes Muster passt, und liefert sonst den ersten Wert, der nicht passte. Binaries werden nach Größe und Typ zerlegt, wie 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")Kontrollfluss
(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 ist eine lazy Comprehension mit :let, :when und :while; doseq ist das eifrige Gegenstück für Seiteneffekte.
Collections
Collections sind persistent: Eine Änderung liefert einen neuen Wert und lässt den alten unberührt. Darunter liegen gewöhnliche BEAM-Werte — Listen, Maps, MapSets, Binaries — die ohne Umwandlung nach Erlang und Elixir wechseln.
(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))Subjekt zuerst gilt auch dort, wo Clojure es anders macht: (seq/cons (list 2 3) 1) ergibt (1 2 3). Eine Reduktionsfunktion nimmt das Element zuerst und den Akkumulator als zweites. Eine Map hat keine Reihenfolge, deshalb liefert map/keys ihre Schlüssel in der Reihenfolge, in der die Map sie gerade hält.
Records, Protokolle und Typen
(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) ; => falseEin Record ist ein Elixir-Struct und behält wie ein Typ seinen Großbuchstaben; jeder andere Name in Praxis-Code ist kleingeschrieben. Ein Protokoll lebt in einem eigenen Namespace — ein Protokoll pro Namespace — und defimpl nennt diesen Namespace; wer das Sequenz-Protokoll implementiert, macht einen Record für jede seq/-Funktion und für Elixirs Enum nutzbar. deftype definiert aus Mustern ein Prädikat name?, für Specs und Guards.
Namespaces und Interop
Jede Datei beginnt mit (ns name "docstring" …). Die Standardbibliothek braucht kein require: string/, seq/, map/, io/ und die übrigen stehen ab der ersten Zeile bereit. Host-Module kommen auf demselben Weg herein wie Praxis-Module:
(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"Definitionen tragen Metadaten — ^{:added "0.1.0"} und Ähnliches — die in den Standard-Dokumentationsblock nach EEP-48 wandern; so lesen auch Erlangs Shell und ExDoc Praxis-Dokumentation. Aus Elixir gesehen ist ein Praxis-Namespace ein Modul: shop.core ist :"praxis.shop.core". Eine Definition darf keinen Namen übernehmen, den praxis.core schon hat (PRX0128), es sei denn, die Datei sagt es mit (refer-praxis :exclude [name]).
Makros
(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 löst Symbole im Namespace auf, der das Makro definiert, und ein angehängtes # erzeugt einen frischen Namen — Makros sind also von Haus aus hygienisch. defsigil definiert ein neues ~x"…"-Literal und nimmt einen Docstring, eine :spec und einen Rumpf beliebiger Länge; seine Expansion muss literale Daten sein.
Prozesse und OTP
spawn, send und receive werden auf jeder Maschine gleich geschrieben: native Prozesse auf der BEAM, Green Processes in JavaScript. receive ist selektiv und nimmt Guards und ein (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 ist in Praxis-Begriffen vorhanden: defbehaviour deklariert ein Behaviour, (:behaviour …) im ns übernimmt eines; praxis.actor kapselt gen_server, praxis.supervisor baut Supervision-Bäume. Ein kompiliertes Projekt ist eine OTP-Anwendung, und prx release baut ein Release. Supervision läuft auch unter Node. Siehe das Beispiel mit überwachtem Actor.
Fehler
Ein Fehler ist ein Name und eine Map. throw löst einen aus; eine einzige catch-Klausel bindet beides; finally läuft immer, und rethrow reicht den Fehler weiter. Fehler aus dem Host werden in dieselbe Form übersetzt.
(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"Für Werte, die scheitern können, sind #(:ok …) und #(:error …) das Idiom, und praxis.result arbeitet mit ihnen, statt auf sie zu matchen — für die Fälle, die with schlecht abdeckt: jeden Fehler statt nur den ersten, eine Collection von Ergebnissen, und beide Hälften eines Stapels, der teilweise gescheitert ist. Benannte Fehler mit Feldern werden mit deferror in einem Namespace <base>.errors deklariert.
(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-Meldungen tragen Codes und zeigen auf die Quelle:
(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
Zeichenketten
Eine Zeichenkette ist ein UTF-8-Binary. String-Interpolation gibt es noch nicht — sie steht auf der Ideenliste, nicht im Plan. Zeichenketten entstehen mit str und dem Namespace string/:
(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"
Die letzte Zeile ist die Mehrdeutigkeit, die Erlang schon immer hatte, jetzt sichtbar: Eine Liste druckbarer Zeichencodes wird als die Charlist ausgegeben, die sie sein könnte, weil nichts im Wert sagt, was gemeint war.
Standardbibliothek
Jede Definition der Standardbibliothek hat einen Docstring und ein Beispiel, fast alle auch eine :spec. Die Beispiele laufen im Gate als Tests; eines, dessen Ergebnis von der Maschine abhängt, etwa das eines Sockets, ist als nur der Form nach gezeigt markiert.
| Bereich | Namespaces |
|---|---|
| Daten | seq stream collect map set vector tuple keyword queue walk result spec function |
| Zahlen | integer float decimal math bits random |
| Text und Bytes | string inspect re bytes codec json uri crypto ansi pxn |
| System | io file path time system code app logger |
| Nebenläufigkeit | process actor supervisor atom kv pool cache node |
| Netzwerk | net.tcp net.udp net.tls net.socket net.dns — deliberately no HTTP client |
| Browser | dom event storage history location service-worker |
praxis.time kennt vier Arten von Zeit — einen Zeitpunkt, ein Datum, eine Uhrzeit und ein lokales Datum mit Uhrzeit — und instant?, date?, clock? und local? sagen, welche ein Wert ist.
Projekte
Ein Projekt wird durch project.pxn beschrieben, das selbst Praxis-Daten ist. prx new shop --app schreibt dies, zusammen mit Quellen, Tests, einer README, einem Changelog und einer Lizenz:
(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]}]]})Das Test-Framework, sein Plugin für prx test und der Formatter sind von Anfang an deklariert. Abhängigkeiten kommen aus Hex, Git, einem Pfad oder demselben Workspace: [jason :hex "~> 1.4"], [thing :git "https://…" {:tag "v1.0"}], [local :path "../local"]. Ein Manifest kann außerdem ein Layout wählen — ein einzelnes Projekt, einen Workspace aus Paketen oder eine Suite aus Apps und Bibliotheken. Laufzeitkonfiguration ist 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