Tutorial: the Logos language
Copy MarkdownThis tutorial introduces the Logos language itself -- the Lisp dialect --
independent of how it's embedded in a host application. Every example here
can be typed straight into mix logos.repl, or fed to
Logos.eval_string_sequence/2 if you're following along from Elixir (see
the library tutorial for that side). For a systematic,
lookup-style reference once you know your way around, see
LOGOS.md.
1. Literals
42 ; integer (arbitrary precision)
3.14 ; float
1/3 ; ratio -- auto-reduced, auto-demoted to an integer when den=1
"hello" ; string (UTF-8)
\a ; character
\newline ; named character
:keyword ; keyword -- self-evaluating, never resolved against an environment
nil ; nil
true false ; booleans
(1 2 3) ; list -- NOT self-evaluating, see below
[1 2 3] ; vector -- self-evaluating
{:a 1 :b 2} ; map -- self-evaluating
#{1 2 3} ; set -- self-evaluatingEverything except a list is self-evaluating: type 42 at the REPL and
you get 42 back. A list, though, is Logos code: (1 2 3) tries to call
1 as a function, which fails. To get a literal list as data, quote it:
(quote (1 2 3))
;;=> (1 2 3)
'(1 2 3) ; ' is reader sugar for (quote ...)
;;=> (1 2 3)2. cond -- the one primitive conditional
cond takes test/expression pairs and evaluates only the first taken
branch -- nothing else in this language ever conditionally skips
evaluation except cond itself (and things built from it):
(cond
false 1
false 2
true 3)
;;=> 33. do -- sequencing
(do 1 2 3)
;;=> 3Runs every form in order, evaluating each for side effect, and returns the
value of the last one. You'll mostly see do inside fn/let/try
bodies, which already accept multiple forms without needing an explicit
wrapping do.
4. def -- defining things
(def x 10)
;;=> user/x -- def's own result is the defined symbol, not the value
x
;;=> 10def interns a Var into the current namespace (user by default). It's
a special form, not a function, because its first argument (the name)
must not be evaluated.
5. fn -- functions
(fn [x y] (+ x y))
;;=> #<Fn 2>
((fn [x y] (+ x y)) 3 4)
;;=> 7
;; &rest params:
((fn [a & rest] rest) 1 2 3)
;;=> (2 3)
;; multi-arity:
(fn ([a] a) ([a b] (+ a b)))fn captures its lexical environment -- a closure. Combine def and
fn to name a function (this is exactly what the defn macro below does
for you):
(def add (fn [a b] (+ a b)))
(add 2 3)
;;=> 5A self-recursive fn in tail position runs with true tail-call
optimization -- no Elixir call-stack growth, no matter how many
iterations. This is validated for real in this project's own test suite
with a 10,000,000-iteration test.
6. try / throw / catch / finally
(try
(throw :not-found "no such record")
(catch :not-found e e)
(finally nil))
;;=> "no such record"throw takes a keyword tag and a value; catch matches by tag with =
(no class hierarchy). finally, if present, always runs. try/catch
also catches a primitive-level failure (division by zero, an unbound
symbol, a wrong arity, ...), not just an explicit (throw ...) -- its
catch tag is derived from the failure itself (e.g. (catch :division-by-zero e e)), or catch :error as a wildcard for any
primitive-level failure; see LOGOS.md for the full
derivation rules.
7. The stdlib macros: if/let/when/unless/and/or/defn
Everything past this point is not a special form -- it's an ordinary
macro, written in Logos itself, in priv/stdlib/core.logos (the
logos.core namespace), and loaded into every runtime via
Logos.new_runtime/1.
(if (> 3 2) :yes :no)
;;=> :yes
(let [x 1
y (+ x 1)]
(+ x y))
;;=> 3
(when true 1 2 3) ; => 3 (nil if the test is false)
(unless false :yes) ; => :yes (nil if the test is true)
(and 1 2 3) ; => 3 (last value, real Clojure semantics)
(or false nil 5) ; => 5
(defn add [a b] (+ a b))
(add 2 3)
;;=> 5if is genuinely just a macro over cond
(`(cond ~test ~then true ~else)) -- there's no separate if
evaluator code path. and/or short-circuit and evaluate each argument
at most once, using a hygienically gensym'd temporary internally so your
own code's use of a similarly-named variable is never captured.
defn (and defmacro) accept an optional leading docstring:
(defn add
"Adds two numbers."
[a b]
(+ a b))
(doc add)
;;=> "Adds two numbers."defn- is defn plus ^:private on the resulting Var (excluded from
require/use :refer, still reachable by a qualified reference).
8. Namespaces
(in-ns 'mynamespace)
;;=> mynamespace
(def y 5)
;;=> mynamespace/y
y
;;=> 5in-ns switches (creating if needed) the current namespace. Every
namespace except logos.core implicitly sees logos.core's public vars
(if/let/+/... work everywhere without qualification), matching
Clojure's implicit clojure.core refer.
Namespace names can contain dots, matching Clojure's own my-app.core
convention -- (in-ns 'my.app) reads and switches into a namespace
literally named "my.app" (a dotted name has no special segment-based
meaning to Logos itself; it's just part of the string), and require/
use/import accept the same dotted names:
(in-ns 'my.app)
;;=> my.appns is sugar over in-ns plus one require/use call per clause:
(ns my.app
(:require other-ns)
(:use [another-ns :refer [a b]]))require/use (and therefore ns) resolve a namespace already loaded
into the same Runtime first; otherwise they fall back to real
file-based loading off disk, against however the embedding host
configured that Runtime's :load_paths -- see
LOGOS.md for the full namespace
reference and CONTRIBUTING.md for the project's
honest list of current gaps.
9. defmacro and syntax-quote
defmacro itself is not a special form -- it's bootstrapped at the very
top of priv/stdlib/core.logos, conceptually equivalent to:
(def ^:macro defmacro
(fn [name params & body]
(list 'def (with-meta name {:macro true}) (cons 'fn (cons params body)))))defmacro is just def plus flagging the resulting Var as a macro (via
^:macro/with-meta, or the set-macro! primitive directly) -- a macro
is an ordinary function value, distinguished only by a :macro true
flag on its Var's metadata. The real bootstrap is a little more involved
than this, since defmacro itself also accepts an optional leading
docstring ((defmacro name "doc" [params] body...), same as defn) --
see priv/stdlib/core.logos for the exact version. Once defmacro
exists, you can write your own macros, and from here on you'll almost
always want syntax-quote instead of hand-building list/cons calls:
(defmacro my-if [test then else]
`(cond ~test ~then true ~else))
(my-if true 1 2)
;;=> 1` (syntax-quote) is like quote, but ~x splices in the value of
x (evaluated at macro-expansion time) and ~@x splices in the elements
of a list x directly, without an extra wrapping level:
(defmacro my-when [test & body]
`(cond ~test (do ~@body) true nil))
(my-when true 1 2 3)
;;=> 3Two more things syntax-quote does automatically, both worth knowing before you write anything nontrivial:
Auto-qualification: every bare symbol inside a syntax-quote that isn't a param/local resolves and qualifies against wherever it actually points right now (so a macro can safely refer to
cond/let/anything else fromlogos.corewithout worrying which namespace expands it).Auto-gensym: a symbol ending in
#(e.g.g#) is consistently renamed to a fresh, unique symbol within one syntax-quote -- this is exactly how the realand/ormacros avoid capturing a caller's own variable of the same name:;; from priv/stdlib/core.logos: (defmacro and [& forms] (cond (= forms ()) true (= (rest forms) ()) (first forms) true `(let [g# ~(first forms)] (cond g# (and ~@(rest forms)) true g#))))
10. Sequences and maps: map/filter/reduce, get/assoc
logos.seq (referred into logos.core, so no prefix needed) has the
usual list-walking functions -- list-only, and nil/() both count as
empty:
(map #(+ % 1) (list 1 2 3))
;;=> (2 3 4)
(filter #(> % 2) (list 1 2 3 4 5))
;;=> (3 4 5)
(reduce + (list 1 2 3 4 5))
;;=> 15
(reduce + 100 (list 1 2 3))
;;=> 106
(take 2 (list 1 2 3 4))
;;=> (1 2)
(drop 2 (list 1 2 3 4))
;;=> (3 4)
(count (list 1 2 3))
;;=> 3into pours a source collection into a target, the target's own shape
deciding how:
(into [] (list 1 2 3))
;;=> [1 2 3]
(into #{} (list 1 2 2 3))
;;=> #{1 2 3}Maps support get/assoc/dissoc, plus Clojure-style keyword-as-function
lookup:
(def m {:a 1 :b 2})
(get m :a)
;;=> 1
(get m :missing :default)
;;=> :default
(:a m)
;;=> 1
(assoc m :c 3)
;;=> {:a 1 :b 2 :c 3}
(dissoc m :a)
;;=> {:b 2}get (and (:key coll)) also work on a set (membership-as-lookup: the
element itself if present, nil/a given default otherwise) and, for
get/assoc, a vector (by index) -- dissoc stays map-only, matching
real Clojure.
11. Concurrency: spawn/send/receive/atom
Every process here is a genuine BEAM process -- no simulation layer.
(def pong-count (atom 0))
(def pinger
(spawn (fn []
(receive [msg]
((= msg :ping) (swap! pong-count (fn [x] (+ x 1))))
(after 200 :gave-up)))))
(send pinger :ping)
;;=> :ping
;; (give the spawned process a moment to run, then:)
(deref pong-count)
;;=> 1receive is a macro: [msg] names the received message, each
(test handler) pair is tried in order against one popped message, and an
optional trailing (after timeout-ms default-expr) clause bounds how long
to wait. Atoms (atom/deref/swap!/reset!) are ordinary functions
built entirely on spawn/send/receive -- a small self-recursive Lisp
loop process holds the state, exactly the canonical Erlang
"process holding state" pattern, not a special runtime type.
12. Putting it together: a tiny worked program
A self-recursive counter, driven entirely through message passing --
demonstrates defn, tail recursion, spawn, receive/after, and
atom together:
(defn counter-loop [n]
(receive [msg]
((= (first msg) :bump) (counter-loop (+ n 1)))
((= (first msg) :report) (do (send (first (rest msg)) n) (counter-loop n)))
(after 1000 n)))
(def result (atom nil))
(def counter (spawn (fn [] (counter-loop 0))))
(send counter (list :bump))
(send counter (list :bump))
(send counter (list :bump))
(send counter (list :report (self)))
(receive [n] (true (reset! result n)))
(deref result)
;;=> 3(Every message here is a Logos list, (:bump)/(:report pid), tagged
by its first element -- not a bare keyword. first/rest only work on
lists/nil, so a message needs to actually be a list before a receive
clause can pull a tag out of it this way.)
counter-loop is its own tiny stateful process, updated by tail-recursive
self-calls (real TCO -- this can run forever without growing the Elixir
stack) rather than any mutable variable. From here: LOGOS.md
for the full reference, LOGOS_EXAMPLES.md for more
worked programs, and LOGOS_CHEATSHEET.md for a
scannable quick-reference.