Concepts
Seqs
TBD
Macros
Like many Lisps, Basilisp supports extending its syntax using macros.
Macros are created using the defmacro
macro in basilisp.core
.
Syntax for the macro usage generally matches that of the sibling defn
macro, should be a relatively easy transition.
Once a macro is defined, it is immediately available to the compiler. You may define a macro and then use it in the next form!
The primary difference between a macro and a standard function is that macros are evaluated at compile time and they receive unevaluated expressions, whereas functions are evaluated at runtime and arguments will be fully evaluated before being passed to the function. Macros should return the unevaluated replacement code that should be compiled. Code returned by macros must be legal code – symbols must be resolvable, functions must have the correct number of arguments, maps must have keys and corresponding values, etc.
Macros created with defmacro
automatically have access to two additional parameters (which should not be listed in the macro argument list): &env
and &form
.
&form
contains the original unevaluated form (including the invocation of the macro itself).
&env
contains a mapping of all symbols available to the compiler at the time of macro invocation – the values are maps representing the binding AST node.
Note
Being able to extend the syntax of your language using macros is a powerful feature. However, with great power comes great responsibility. Introducing new and unusual syntax to a language can make it harder to onboard new developers and can make code harder to reason about. Before reaching for macros, ask yourself if the problem can be solved using standard functions first.
Warning
Macro writers should take care not to emit any references to Private Vars in their macros, as these will not resolve for users outside of the namespace they are defined in, causing compile-time errors.
See also
Syntax Quoting, quote
, gensym
, macroexpand
, macroexpand-1
, unquote
, unquote-splicing
Binding Conveyance
TBD
Destructuring
The most common type of name binding encountered in Basilisp code is that of a single symbol to a value.
For example, below the name a
is bound to the result of the expression (+ 1 2)
:
(let [a (+ 1 2)]
a)
In many cases this form of name binding is sufficient.
However, when dealing with data nested in vectors or maps of known shapes, it would be much more convenient to bind those values directly without needing to write collection accessor functions by hand.
Basilisp supports a form of name binding known as destructuring, which allows convenient name binding of values from within sequential and associative data structures.
Destructuring is supported everywhere names are bound: fn
argument vectors, let
bindings, and loop
bindings.
Note
Names without a corresponding element in the data structure (typically due to absence) will bind to nil
.
Sequential Destructuring
Sequential destructuring is used to bind values from sequential types.
The binding form for sequential destructuring is a vector.
Names in the vector will be bound to their corresponding indexed element in the sequential expression value, fetched from that type as by nth
.
As a result, any data type supported by nth
natively supports sequential destructuring, including vectors, lists, strings, Python lists, and Python tuples.
It is possible to collect the remaining unbound elements as a seq
by providing a trailing name separated from the individual bindings by an &
.
The rest element will be bound as by nthnext
.
It is also possible to bind the full collection to a name by adding a trailing :as
name after all binding forms and optional rest binding.
(let [[a b c & others :as coll] [:a :b :c :d :e :f]]
[a b c others coll])
;;=> [:a :b :c (:d :e :f) [:a :b :c :d :e :f]]
Sequential destructuring may also be nested:
(let [[[a b c] & others :as coll] [[:a :b :c] :d :e :f]]
[a b c others coll])
;;=> [:a :b :c (:d :e :f) [[:a :b :c] :d :e :f]]
Associative Destructuring
Associative destructuring is used to bind values from associative types.
The binding form for associative destructuring is a map.
Names in the map will be bound to their corresponding key in the associative expression value, fetched from that type as by get
.
Asd a result, any associative types supported by get
natively supports sequential destructuring, including maps, vectors, strings, sets, and Python dicts.
It is possible to bind the full collection to a name by adding an :as
key.
Default values can be provided for keys by providing a map of binding names to default values using the :or
key.
(defn f [{x :a y :b :as m :or {y 18}}]
[x y m])
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
(f {:a 1}) ;;=> [1 18 {:a 1}]
(f {}) ;;=> [nil 18 {}]
For the common case where the names you intend to bind directly match the corresponding keyword name, you can use the :keys
notation.
(defn f [{:keys [a b] :as m}]
[a b m])
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
(f {:a 1}) ;;=> [1 nil {:a 1}]
(f {}) ;;=> [nil nil {}]
There exists a corresponding construct for the symbol and string key cases as well: :syms
and :strs
, respectively.
(defn f [{:strs [a] :syms [b] :as m}]
[a b m])
(f {"a" 1 'b 2}) ;;=> [1 2 {"a" 1 'b 2}]
Note
The keys for the :strs
construct must be convertible to valid Basilisp symbols.
It is possible to bind namespaced keys directly using either namespaced individual keys or a namespaced version of :keys
as :ns/keys
.
Values will be bound to the symbol by their name only (as by name
) – the namespace is only used for lookup in the associative data structure.
(let [{a :a b :a/b :c/keys [c d]} {:a "a"
:b "b"
:a/a "aa"
:a/b "bb"
:c/c "cc"
:c/d "dd"}]
[a b c d])
;;=> ["a" "bb" "cc" "dd"]
Keyword Arguments
Basilisp functions can be defined with support for keyword arguments by defining the “rest” argument in an defn
or fn
form with associative destructuring.
Callers can pass interleaved key/value pairs as positional arguments to the function and they will be collected into a single map argument which can be destructured.
If a single trailing map argument is passed by callers (instead of or in addition to other key/value pairs), that value will be joined into the final map.
(defn f [& {:keys [a b] :as kwargs}]
[a b kwargs])
(f :a 1 :b 2) ;;=> [1 2 {:a 1 :b 2}]
(f :a 1 {:b 2}) ;;=> [1 2 {:a 1 :b 2}]
(f {:a 1 :b 2}) ;;=> [1 2 {:a 1 :b 2}]
Note
Basilisp keyword arguments are distinct from Python keyword arguments. Basilisp functions can be defined with Python compatible keyword arguments but the style described here is intended primarily for Basilisp functions called only by other Basilisp functions.
Warning
The trailing map passed to functions accepting keyword arguments will silently overwrite values passed positionally. Callers should take care when using the trailing map calling convention.
(defn f [& {:keys [a b] :as kwargs}]
[a b kwargs])
(f :a 1 {:b 2 :a 3})
;;=> [3 2 {:a 3 :b 2}]
Nested Destructuring
Both associative and sequential destructuring binding forms may be nested within one another.
(let [[{:keys [a] [e f] :d} [b c]] [{:a 1 :d [4 5]} [:b :c]]]
[a b c e f])
;;=> [1 :b :c 4 5]
References and Refs
TBD
Transducers
TBD
Hierarchies
TBD
Multimethods
TBD
Protocols
TBD
Data Types
TBD
Records
TBD