Sprachreferenz
Die Sprache mit Stand v0.60.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
#(: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 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"
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 (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 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) ; => 10recur erzeugt auf jeder Maschine eine flache Schleife; es muss in Endposition stehen. defn- definiert eine private Funktion.
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
(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}
(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))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.
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. 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".
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; 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 …) mit praxis.result das Idiom: (result/collect [#(:ok 1) #(:error :a) #(:error :b)]) ergibt #(:error (:a :b)) — jeden Fehler, nicht nur den ersten. Benannte Fehler mit Feldern werden mit deferror in einem Namespace <base>.errors deklariert.
Compiler-Meldungen tragen Codes und zeigen auf die Quelle:
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)
| ^^^^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
Standardbibliothek
Jede Definition der Standardbibliothek hat einen Docstring und ein ausgeführtes Beispiel, fast alle auch eine :spec.
| Bereich | Namespaces |
|---|---|
| Daten | seq stream collect map set vector tuple keyword queue walk result spec |
| Zahlen | integer float decimal math bits random |
| Text und Bytes | string re bytes codec json uri crypto ansi pxn |
| System | io file path time system code app logger cli |
| 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 |
Projekte
Ein Projekt wird durch project.pxn beschrieben, das selbst Praxis-Daten ist. prx new shop --app schreibt dies, zusammen mit Quellen, Tests und einer 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]}]]})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