Tutorial on Using Explang with Lisp Syntax
Expressions
An Explang program is a sequence of one or more expressions which are evaluated from the beginning to the end.
The language works with expressions that return values. There are no statements.
The simplest expressions are things like numbers, booleans or strings, which evaluate to themselves.
> 10
=> 10
> true
=> true
> "foo bar"
=> foo bar
Several expressions enclosed between parentheses are also an expression, they are called lists. When a list is evaluated its first item is considered as function and the rest of items are passed as parameters. In most cases parameters are evaluated from left to right and then the operation uses their computed values as arguments. The return value of the operation is the value of the expression.
For example,
> (+ 1 2 3)
=> 6
First the sub-expressions +
, 1
, 2
and 3
were evaluated, returning the
plus function, 1
, 2
and 3
. 1
, 2
and 3
were then passed to the plus
function, which returned 6.
Such expressions may be complex:
> (+ (* 2 3) 1)
=> 7
Comments
A semicolon ;
starts a comment. The comment continues until end of line.
> "foo" ; a comment
=> "foo"
Symbols and their Values
The expressions +
and *
here are Symbols.
Symbols don't evaluate to themselves but return values they have been assigned.
If we give b
the value 10, it will return 10 when evaluated:
> (setv b 10)
=> 10
> b
=> 10
If we try to evaluate a Symbol that has not been assigned a value we'll get error[^1].
Note that evaluation of (setv b 10)
when b
still was unassigned did
not cause an error. This is because there are some operations (they
are called special operators or special forms) that do not conform the usual
evaluation rule, and setv
is one of them. Its first argument isn't
evaluated.
The symbols that start with letter ':' are special, they are
keywords
-- symbols that are always assigned values of themselves.
> :something
=> :something
They may be used for different kinds of identifiers: keys in map, names of parameters, etc.
Quoting
You can turn off evaluation by using the quote
special form:
> (quote b)
=> b
This can be abbreviated by putting a single quote character before an expression:
> 'b
=> b
When you quote a list, you get back the list itself.
> (+ 1 2)
=> 3
> '(+ 1 2)
=> [+, 1, 2]
The first expression returns the number 3. The second, because it was quoted, returns a list consisting of the symbol + and the numbers 1 and 2.
Creating Lists
To create a list use the list
function:
> (list 1 (+ 2 3) "foo" 'b)
=> [1, 5, foo, b]
Defining Functions
We've already seen some functions: +
, *
, list
. You can define
new ones with defun
, which takes a symbol to use as the name, a list
of symbols that describe the parameters, and then zero or more
expressions called the body. When the function is called, those
expressions will be evaluated in order with the parameter symbols in
the body temporarily set ('bound') to the corresponding argument
values. Whatever the last expression returns will be returned as the
value of the call. Here's a function that takes two numbers and
returns their average:
> (defun average (x y)
(/ (+ x y) 2))
=> io.opsit.explang.Compiler$LAMBDA$1@5e9f23b4
The body of the function consists of one expression, (/ (+ x y) 2)
It is common for functions to consist of one expression; in purely
functional code (code with no side-effects) they always do.
Notice that defun
, like setv
, doesn't evaluate all its arguments.
It is another of those special forms with its own evaluation rule.
What is the object returned as the value of the def expression? That's the Java Object that represents the function.
Now the symbol average
is assigned function value and you can invoke
it in the same way you invoke the built-in functions like +
or *
:
> (average 100 200)
=> 150
As the literal representation of a string is a series of characters
surrounded by double quotes, the literal representation of a function
is a list consisting of the symbol lambda
, followed by its
parameters, followed by its body. So you could represent a function to
return the average of two numbers as:
> (lambda (x y) (/ (+ x y) 2))
=> io.opsit.explang.Compiler$LAMBDA$1@37a71e93
And can use a literal function wherever you could use a symbol whose value is one, e.g.
> ((lambda (x y) (/ (+ x y) 2)) 100 200)
=> 150
Function Values
Notice that if you try to get the function object simply by referencing the average
symbol you get an error.
> average
EXECUTION ERROR: java.lang.RuntimeException: variable 'average' does not exist in this context at:
This is because of important property of Explang: named functions and ordinary data variables are
living in separate namespaces, that is you may assign value to variable average
without
overwriting the function average
:
> (setv average 100)
=> 100
> (average average 200)
=> 150
To reference the value of the defined function one must use the function
special operator,
which can be abbreviated as #'
:
> (function average)
=> io.opsit.explang.Compiler$LAMBDA$1@5e9f23b4
> #'average
=> io.opsit.explang.Compiler$LAMBDA$1@5e9f23b4
And to set the function binding one can use the fset
function, all the def does is
basically:
(fset 'average (lambda (x y) (/ (+ x y) 2)))
Getting Help on Functions
To get short description of a function or a special operator
use describe-function
.
> (describe-function 'random)
=> random is a built-in function defined at class io.opsit.explang.Funcs$RANDOM
Arguments: limit
Documentation:
Produce Pseudo-Random Number. Returns a pseudo-random number that is a non-negative number less than limit and of the same numeric type as limit. Implemented uding Java Math.random()
Package: base.math
describe-function
works both with built-in as well with the
user-defined functions. For the latter ones the function description
is the value of so called documentation strings - "docstrings". if
the first expression in the function is a string literal it is
considered to be documentation string:
> (defun average (x y)
"Compute average of two numbers."
(/ (+ x y) 2))
=> io.opsit.explang.Compiler$LAMBDA$1@37a71e93
> (describe-function 'average)
=> average is a compiled function defined at INPUT1:line=3:pos=15:o=74:len=0
Arguments: x y
Documentation:
Compute average of two numbers.
Package: user
To get list of all the available function use functions-names
:
> (functions-names )
=> [%, *, +, -, ->, ->>, ., .N, .S, /, <, <=, =, ==, ===, >, >=, @->, AND, APPEND
...
Printing values
So far we've only had things printed out implicitly as a result of
evaluating them. The standard way to print things out in the middle of
evaluation is with print
or println
. They take multiple arguments and print
them in order; println
also prints a newline at the end. Here's a variant
of average that tells us what its arguments were:
> (defun average (x y)
(println "my arguments were: " (list x y) "\n")
(/ (+ x y) 2))
> (average 100 200)
my arguments were: [100 200]
=> 150
Return Value of the Function
From the previous example we see that when a function contains several expressions the return value of the function is the value of the last expression in the function. Previous expressions are evaluated and their value is discarded.
If you want to return value from function without reaching the last
expression use the return
operator.
In the following example we define function that solves quadratic equations. In the case when determinant is negative we print an error message and exit prematurely returning an empty list.
> (defun quadratic-eq-roots(a b c)
"Finds real roots of an quadratic equation"
(setv discriminant (- (* b b) (* 4 a c)))
(when (< discriminant 0)
(println "Error: the equation has no real solutions!")
(return ()))
(list (/ (- (- b) (sqrt discriminant)) (* 2 a))
(/ (+ (- b) (sqrt discriminant)) (* 2 a))))
> (quadratic-eq-roots 2 4 -4)
=> [-2.732050807568877, 0.7320508075688772]
> (quadratic-eq-roots 1 2 3)
Error: the equation has no real solutions!
=> []
Optional and Rest Arguments for Functions
We’ve seen above some functions like list
and +
that take varying
numbers of arguments. Now we'll learn to define such functions.
Function parameters list can contain extra directives that specify how the following parameters will be handled.
To make the parameters optional use the &OPTIONAL
directive:
(defun greet (name &OPTIONAL o)
(str "hello " name o))
=> io.opsit.explang.Compiler$LAMBDA$1@50040f0c
[16]> (greet "Joe" "!!!! :-)")
=> hello Joe!!!! :-)
[17]> (greet "Joe")
=> hello Joe
When optional argument is not specified it gets its default value, which is NIL by default. To specify default value specify the parameter as list of form (parameter default_value). The default value does not have to be a constant. If you put an expression after the name of an optional parameter, it will be evaluated if necessary to produce a default value. The expression can refer to preceding parameters.
> (defun greet (name &OPTIONAL (t1 "")
(t2 (if (== t1 "Mr.") ", Sir"
(if (== "Mrs." t1) ", Ma'am"))))
(str "Hello " t1 " " name "! Nice to see you" t2 "!"))
=> io.opsit.explang.Compiler$LAMBDA$1@6193b845
> (greet "Figman" "Mrs.")
=> Hello Mrs. Figman! Nice to see you, Ma'am!
> (greet "Alice")
=> Hello Alice! Nice to see you!
One can return to defining mandatory arguments after optional ones, but only one such sequence of optional arguments can be defined (otherwise there would be more than one way to associate values with the defined arguments).
> (defun join2 (x &OPTIONAL (sep ", ") &REQUIRED y)
(str x sep y))
=> io.opsit.explang.Compiler$LAMBDA$1@5d099f62
> (join2 "x" "y")
=> x, y
> (join2 "x" " and " "y")
=> x and y
To make a function that takes any number of arguments we can specify a rest parameter using the '&REST' directive. This parameter will contain list of all the values of the remaining arguments:
> (defun foo (x y &REST z)
(list x y z))
=> io.opsit.explang.Compiler$LAMBDA$1@37f8bb67
> (foo (+ 1 2) (+ 3 4) (+ 5 6) (+ 7 8))
=> [3, 7, [11, 15]]
This type of parameter is called a “rest parameter” because it gets the rest of the arguments. If you want all the arguments to a function to be collected in one parameter, just use it in place of the whole parameter list.
To supply a list of arguments to a function we can use the apply
function:
(setv L (list 1 2 3))
=> [1, 2, 3]
> (apply #'+ L)
=> 6
Now that we have rest parameters and apply, we can write a version of average that takes any number of arguments.
(defun average (&REST args)
(/ (apply #'+ args)
(length args)))
=> io.opsit.explang.Compiler$LAMBDA$1@3d646c37
> (apply #'average 1 2 3)
=> 2
Keyword arguments
Keyword arguments are used when we want to pass a specific value to a specific argument. Consider a scenario where there are 3 optional arguments, what if the user wants to pass just a second optional argument’s value, here the first arguments even though is optional it will be mandatory for him to pass its value making it a required argument.
To solve this problem keyword arguments are used where the caller has a choice to specify for argument values to which particular argument they will be passed.
To define keyword arguments in a function, add the symbol &KEY before the arguments that are supposed to be keyword arguments.
> (defun keyword_example (&KEY x y z) (list x y z))
=> io.opsit.explang.Compiler$LAMBDA$1@41cf53f9
Keyword argument are passed as pairs of argument keywords and their values:
> (keyword_example :z 3 :x 1 :y 2 )
=> [1, 2, 3]
The keyword arguments are optional, you can call this function without
setting all the arguments, the default argument value is NIL
.
> (keyword_example)
=> [null, null, null]
(keyword_example :x 1 :y 2)
=> [1, 2, null]
You can set default value of the keyword arguments just like with optional positional arguments:
> (defun keyword_example (&KEY (x 1) (y 2) (z (+ x y))) (list x y z))
=> io.opsit.explang.Compiler$LAMBDA$1@2ef1e4fa
> (keyword_example)
=> [1, 2, 3]
Very much like with the positional rest parameters you can allow functions to get any number of keyword parameters in addition to those that are named in the argument list.
In the following example the 'r' argument, which is marked with
&REST
and &KEY
, is used to collect pairs of argument names
(keywords) and their values. And the &ALLOW-OTHER-KEYS
allows the
function to accept any keyword arguments.
> (defun other-keys-example (x &REST &KEY r y &ALLOW-OTHER-KEYS) (list x y r))
=> io.opsit.explang.Compiler$LAMBDA$1@5cb0d902
> (other-keys-example 1)
=> [1, null, []]
> (other-keys-example 1 :y 2 :p 3 :q 4)
> [1, 2, [:p, 3, :q, 4]]
Data Types and Values
Explang is a dynamically typed language. There are no type definitions in the language: each value carries its own type and those types are Java types.
The language can work with Java Objects of any type and it has built-in support for standard data types such as Strings, Numbers, Arrays, etc.
To get type of a value we use function type-of
:
> (type-of "foo")
=> class java.lang.String
> (type-of 1.0)
=> class java.lang.Double
To check if object is of some specific type use the typep
operator:
> (typep "abc" 'java.lang.String)
=> true
Base Java type from the java.lang
can be specified without package:
> (typep "abc" 'String)
=> true
The check returns true for the specific class as well as for class parents and the interfaces that it implements:
> (typep "abc" 'CharSequence) ;; String implements CharSequence
=> true
The second arguments can be a string as well as symbol:
> (typep "abc" "Object") ;; String implements CharSequence
=> true
The type of NIL is NIL.
> (typep NIL NIL)
=> NIL
NIL values
NIL represents absence of a useful value, it is implemented as Java null
.
It is not really a data type.
(type-of NIL)
=> NIL
Booleans
The Boolean type has two values, false
and true
. It is implemented using
as Java Boolean objects.
Numeric Types And Values
Explang works with Java numeric types.
Here table of types with examples of literals,
- 1b, 0b -- Byte
- 1s, 2s -- Short (16 bit signed integer)
- 1, -2 -- Integer (32 bit signed integer)
- 1L 0L -- Long literals (64 bit signed integer)
- 0.5f 1.2e-2f -- Float literals (32 bit floating point)
- 0.5 1.2e-2 -- Double literals (64 bit floating point)
> (type-of 1)
=> class java.lang.Integer
> (type-of 1b)
=> class java.lang.Byte
> 0.1e4
=> 1000.0
Numeric Types Promotions
When one performs arithmetic operations between different numeric types Explang performs automatic promotion of numeric types, i.e. the result of the operation will be of the type of the argument with highest precision. Operations between integer types and floating types are promoted to floating point types:
> (type-of (+ 1b 1s))
=> class java.lang.Short
> (type-of (+ 1b 1s 1 1L 1.0))
=> class java.lang.Double
Note that Explang does not perform promotion for overflows between operations:
> (* 100s 1000s) ; 100000 won't fit into 16 bit Short Integer,
; we'll get wrong result
=> -31072
Converting to Numeric Types
The operators byte
, double
, float
, int
, long
, short
convert to corresponding
numeric types from other numeric types as well as from string representation:
> (double 1L)
=> 1.0
> (double "1E4")
=> 10000.0
Strings
Explang strings are Java Strings. Strings are constant, that is one cannot change an existing string but have to create new one.
In code we can delimit literal strings by matching double quotes "
:
> " a line"
=> a line
String literals can have embedded newlines, tabs and any other characters except "
:
> "line1
line2
line3"
=> line1
line2
line3
"
one need to escape it using the \
escape character:
> "quoted word \"foo\""
=> quoted word "foo"
There are other escape characters supported:
- \b - backspace 0x08
- \t - tab 0x09
- \n - newline 0x0a
- \f - form feed 0x0c
- \r - carriage return 0x0d
Other characters when escaped will be inserted as they are:
> "\\t means tab '\t'\n"
=> \t means tab ' '
To convert any object into its string representation use
the string
operator:
> (string (+ 1 2))
=> 3
(string nil)
=> NIL
There are a couple of operators for building strings. The most general is string, which takes any number of arguments and mushes them into a string.
The most general is str
, which takes any number of arguments and
concatenates them into a string. Every argument will appear as it would
look if printed out by pr, except nil, which is ignored.
> (str 99 " bottles of " 'bee #\r)
=> "99 bottles of beer"
To control exactly the way the objects are converted to string use
the format
function, which converts its argument to string
using the first argument as format specification:
(setv x 9786)
(format "Decimal: %d, hexadecimal: %x octal: %o char: %c \n" x x x x)
=> Decimal: 9786, hexadecimal: 263a octal: 23072 char: ☺
uppercase
and lowercase
can be used to change case of a string:
Strings in Explang are considered a kind of indexed sequences
and can be operated upon using generic functions that work with sequences,
for example to get substring one should use subseq
,
to get a specific character one uses get
or areq
and so on.
See below on how to work with sequences.
Character Type
Explang Characters represent Unicode 16bit characters and implemented using Java java.lang.Character.
Character literals are delimited using the #\C
, #\UHEX
or #\ESCAPECHAR
notation:
> '(#\H #\e #\l #\l #\o #\NEWLINE #\u263a )
=> [H, e, l, l, o,
, ☺]
The char
operator allows to create Characters from their numeric code:
> (char 33)
=> !
Characters can be used as operand of arithmetic expressions:
(char (+ 1 #\A))
=> B
Collection and Sequence types
Explang attempts to provide operators that can be used on sequence and collection like objects of different types: lists, arrays, sets, hash maps and so on.
Lists
Lists contains entries (objects or null) in slots that can be addressed by their zero-based index. Lists do not have fixed size, they grow/shrink dynamically when elements are added/removed.
The list
operator creates and initializes a list. The lists created are implemented by
java.util.ArrayList.
> (list 1 2 (list 3 4))
[1, 2, [3, 4]]
Maps
Use the hashmap
to create and initialize a Map
(implemented by java.util.HashMap).
> (hashmap "a" 1 "b" "foo" "c" (hashmap "d" "e"))
=> {a=1, b=foo, c={d=e}}
Sets
A Set is a collection that contains no duplicate elements. More formally, sets contain no pair of elements e1 and e2 such that `(equal e1 e2) return true. As implied by its name, this type models the mathematical set abstraction.
The hashset
operator creates a set and initializes it with the specified elements.
(hashset 1 2 3 7)
=> [1, 2, 3, 7]
(hashset 1 1 (list 1 2 3) (list 1 2 3) NIL NIL)
=> [null, 1, [1, 2, 3]]
Arrays
Arrays are areas of memory that contain entries in slots that can be addressed by their zero-based index. Arrays have fixed size that determined at their creation time. Unlike lists arrays are created to contain objects or primitive values of given type.
The make-array
operator creates array and initializes it with
specified elements.
By defaults make-array
creates array of type Object that can contain any java object.
> (make-array 1 "123" (list 1 2) NIL) ;; array with specified objects
=> [1, 123, [1, 2], null]
> (make-array :size 5) ;; array of specified size
=> [null, null, null, null, null]
> (make-array :size 5 :elementType "double") ;; array for storing primitive double values
=> [0.0, 0.0, 0.0, 0.0, 0.0]
> (make-array :size 10 ; array of size 10
:elementType "char" ; for storing primitive character values
#\H #\E #\L #\L #\O) ; partially filled
=> [0.0, 0.0, 0.0, 0.0, 0.0]
=> [H, E, L, L, O, , , , , ]
Character Sequences
Regular string objects that we've seen earlier is one of the types that are called Character Sequences. In Java these objects implement interface java.lang.CharSequence).
There are other such types that, unlike strings, can be modified in place:
The string-buffer
function creates a
StringBuffer
object:
> (string-buffer) ;; creates empty StringBuffer
=>
> (string-buffer "foo" "bar") ;; create stringbuffer with text
=> "foobar"
string-builder
function creates a
StringBuilder
object.
StringBuilder
is similar to StringBuffer
, but unlike StringBuffer
it can be safely
modified from different threads.
> (string-builder) ;; creates empty StringBuilder
=>
> (string-builder "foo" "bar") ;; create stringbuilder with text
=> "foobar"
The string-builder
function creates a StringBuilder
object:
> (string-builder) ;; creates empty StringBuilder
=>
> (string-builder "foo" "bar") ;; create StringBuilder with text
=> "foobar"
In the following chapters we'll learn to operate on data in sequence types.
Getting Information About Collections and Sequences
The function length
returns length of a collection or sequence:
> (list (length "Hello")
(length (hashmap "a" 1))
(length (hashset 1 2 3))
(length (range 1 10)))
=> [5, 1, 3, 9]
The collection predicates return true
or false
allow to check if an objects have some
property or ability. By convention their name ends by p - predicate.
collp
returns true if the object is a collection of some kind, for example:
> (list (collp ())
(collp (make-array))
(collp "foo") ; string is a CharSequence
(collp (range 1 2))
(collp nil) ;; non-object
(collp 1)) ;; individual object
=> [true, true, true, true, false, false]
seqp
returns true if the object is a sequence, that is collection with specific order.
indexedp
returns true if objects in the collection can be addressed by integer index.
mapp
returns true if it is a Map.
setp
returns true if the object is a Set.
associativep
returns true if objects in collection are indexed or have keys.
in
check if a collection contains an object.
The in
function checks if a collection contains an object.
> (in "foo" (hashset "bar" "baz" "foo" ))
=> true
> (in 1000 (range 1 100))
=> false
> (in #\e "Hi there!")
=> true
Accessing data in collections and sequences
Explang attempts to provide generic data access operators that can be used on all kinds of sequences and collections like lists, arrays, sets, hash maps and so on.
get
- value accessor.
The get
function returns a Sequence element by its index
or a Map entry by its key:
> (setv lst (list "foo" 'bar 1 null))
=> [foo, bar, 1, null]
> (setv m (hashmap "a" 1 "b" "2"))
=> {a=1, b=2}
> (get lst 0)
=> fooo
> (get lst 1)
=> bar
> (get m "a")
=> 1
When provided an invalid index get
returns NIL:
> (get lst 10)
=> NIL
get
accepts an optional third argument -- default value.
If index is invalid or key is missing it will return the value of the
third argument:
> (get lst 10 "No such index")
=> No such index
get-in
access values in nested structures
When the structure is complex like in the following example using get may become cumbersome and require use of several nesting expressions.
> (setv data
(hashmap 'people (hashmap 1 (hashmap 'name "John"
'surname "Doe"
'relatives (hashmap 'motherId 3
'fatherId 2)
'friendIds (list 4))
2 (hashmap 'name "Jack"
'surname "Doe")
3 (hashmap 'name "Ann"
'surname "Roe")
4 (hashmap 'name "Bill"
'surname "Moe"))))
=> {people={1={friendIds=[4], surname=Doe, name=John,
relatives={motherId=3, fatherId=2}}, 2={surname=Doe, name=Jack},
3={surname=Roe, name=Ann}, 4={surname=Moe, name=Bill}}}
`get-in' accepts list of keys to specify path to the needed value:
> (get-in data '(people 1 relatives motherId))
=> 3
When there is no mapping get-in returns provided default value or NIL when no default specified:
> (get-in data '(people 2 relatives motherId))
=> NIL
> (get-in data '(people 2 relatives motherId) "N/A")
=> N/A
`get-in' works not only with Maps but with lists and other sequences:
> (get-in data '(people 1 friendIds 0))
=> 4
> (get-in data '(people 1 name 0))
=> J
select-keys
select entries by their keys/indices
Returns map of entries selected by their keys/indices
> (select-keys (hashmap "a" 1 "b" 2 "c" 3) (list "a" "b"))
=> {a=1, b=2}
> (select-keys (list "a" "b" "c") (list 0 2))
=> {2=c, 0=a}
The returned Map is actually kind of a view into the original object, that is it reflects changes in the original:
> (setv m (hashmap "a" 1 "b" 2))
=> {a=1, b=2}
> (setv s (select-keys m (list "a" "c")))
=> {a=1}
> (assoc! m "c" "3")
=> {a=1, b=2, c=3}
> s
=> {a=1, c=3} ;; now s got new entry c=3
first
first
return first object in collection
> (first (range 2 10))
=> 2
rest
, take
and subseq
rest
returns the subsequence starting from the second element.
take
returns first n elements of the sequence.
subseq
returns a subsequence from the given index or between the first given index and up to the second given index (excluding):
> (setv a (list 1 2 3 4 5 6))
=> [1, 2, 3, 4, 5, 6]
> (first a)
=> 1
> (rest a)
=> [2, 3, 4, 5, 6]
> (subseq a 1)
> [2, 3, 4, 5, 6]
(setv b (subseq a 1 3))
=> [2, 3]
The take
and subseq
return sequence of the same kind as the source sequence.
The type of the returned sequence differs depending on the type of the source sequence.
For lists it returns sublist sharing the elements with the source list.
For arrays this is not possible and the data is copied into new array.
haskey
haskey
checks if a Map has given key or if an indexed sequence has specified index.
Unlike get
and in
this allows to check whether a map contains NIL as a key.
> (setv M (hashmap 1 2 NIL NIL))
=> {null=null, 1=2}
> (haskey M 1)
=> true
> (haskey M NIL)
=> true
> (haskey M 2)
=> false
Modifying data in collections
There are two kinds of functions that modify data in collections: those that change the data in place (mutating) and those that return new data structures with requested changes. By convention the former ones have names that end with exclamation mark.
put!
- put element value into an associative structure.
Set value of element at index/key in the target structure to object.
If target object is a list or an array and index is out of bounds the function returns normally without any change to the target structure.
This function returns previous value of the element or NIL if id did not exist or no change has been made.
Example with a Map:
> (setv m (hashmap "foo" 1))
=> {foo=1}
> (put! m "bar" 77)
=> NIL
> (put! m "foo" 0)
=> 1
> m
{bar=77, foo=0}
Example with an array of integer numbers:
(setv a (make-array :size 5 :elementType "int"))
=> [0, 0, 0, 0, 0]
> (put! a 1 10)
=> 0
> a
=> [0, 10, 0, 0, 0]
put-in!
modify value in a hierarchy of nested associative structures.
Like get-in
this function accepts list of keys to navigate
the nested associative structures (lists, arrays, maps, etc).
Let's add some friends to John using the same structure
from the get-in
example above:
> (setv data
(hashmap 'people (hashmap 1 (hashmap 'name "John"
'surname "Doe"
'relatives (hashmap 'motherId 3
'fatherId 2)
'friendIds (list 4))
2 (hashmap 'name "Jack"
'surname "Doe")
3 (hashmap 'name "Ann"
'surname "Roe")
4 (hashmap 'name "Bill"
'surname "Moe"))))
=> {people={1={friendIds=[4], surname=Doe, name=John,
relatives={motherId=3, fatherId=2}}, 2={surname=Doe, name=Jack},
3={surname=Roe, name=Ann}, 4={surname=Moe, name=Bill}}}
> (put-in! data (list 'people 5) (hashmap 'name "Rick" 'surname "Hoe")) ;; add new person
=> {people={1={friendIds=[4], surname=Doe, name=John, relatives={motherId=3, fatherId=2}}, 2={surname=Doe, name=Jack}, 3={surname=Roe, name=Ann}, 4={surname=Moe, name=Bill}, 5={surname=Hoe, name=Rick}}}
> (put-in! data (list 'people 1 'friendIds 1) 5) ;; add id of the newly added person
=> {people={1={friendIds=[4, 5], surname=Doe, name=John, relatives={motherId=3, fatherId=2}}, 2={surname=Doe, name=Jack}, 3={surname=Roe, name=Ann}, 4={surname=Moe, name=Bill}, 5={surname=Hoe, name=Rick}}}
put-in!
accepts one optional argument that allows to specify how to automatically create
structures along the path in case they do not exist. For example this will
add Rick Row (id=5) as a friend to Bill that does not have a list of friend identifiers.
put-in
would copy the provided object (an empty list) to create list of friendId
s.
(put-in! data (list 'people 4 'friendIds 0) 5 (list))
=> {people={1={friendIds=[4, 5], surname=Doe, name=John, relatives={motherId=3, fatherId=2}}, 2={surname=Doe, name=Jack}, 3={surname=Roe, name=Ann}, 4={friendIds=[5], surname=Moe, name=Bill}, 5={surname=Hoe, name=Rick}}}
delete!
Removes an object from a collection according to its key or index. For sets it will remove the object by its value.
(setv L (list 1 2 3))
=> [1, 2, 3]
> (delete! L 1)
=> 2
> L
=> [1, 3]
insert!
, push!
and pop!
These functions work for lists and mutable character sequences.
insert!
inserts an object at given index:
> (setv L (list 1 2 3))
=> [1, 2, 3]
> (insert! L 1 "X")
[1, X, 2, 3]
push!
will add an object to the end of a sequence, pop!
will remove and return the object at the end of the sequence.
> (setv stack (list))
=> []
> (push! stack "foo")
=> [foo]
> (push! stack "bar")
=> [foo, bar]
> (pop! stack)
=> "bar"
> stack
=> [foo]
APPEND!
append!
adds to the target sequence all the elements of all of the following sequences.
It will return the target sequence. For example:
(setv L (list 1 2 3))
=> [1, 2, 3]
> (append! L (list 4 5 6) (make-array 7 8 9) "HELLO!" )
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, H, E, L, L, O, !]
Target sequence must be mutable extendable, that means that objects like Arrays (non-extendable) or String (immutable) cannot be target of this operation.
reverse!
reverse!
Reverses the order of elements in the given sequence:
> (setv L (list 1 2 3 4 5 6))
=> [1, 2, 3, 4, 5, 6]
> (reverse! L)
=> [6, 5, 4, 3, 2, 1]
> (reverse! (subseq L 1 4))
=> [3, 4, 5]
> L
=> [6, 3, 4, 5, 2, 1]
sort!
sort a sequence.
sort!
will sort a sequence. It can sort
according to the natural order of its objects (if they implement
java.lang.Comparable
) or using user specified compare function.
> (setv L (make-array 1 9 8 8 6))
=> [1, 9, 8, 8, 6]
> (sort! L)
=> [1, 6, 8, 8, 9]
> L
=> [1, 6, 8, 8, 9]
Sort according to length of a sequence with a custom compare function:
> (setv L (list () '(1 2 3 4 5) '(1 2) '(1 2 3) '(1)))
=> [[], [1, 2, 3, 4, 5], [1, 2], [1, 2, 3], [1]]
> (sort! (lambda (a b)
(- (length a) (length b)))
L)
=> [[], [1], [1, 2], [1, 2, 3], [1, 2, 3, 4, 5]]
> L
=> [[], [1], [1, 2], [1, 2, 3], [1, 2, 3, 4, 5]]
Collection Specific Access functions
In addition to the generic data access functions there are data access functions that are specific for concrete collection types.
aref
and aset!
-- array member access.
Unlike put!
and get
they will fail with exception when index is invalid.
> (setv L '(1 2 3))
=> [1, 2, 3]
> (aset! L 1 0)
=> 2
> L
=> [1, 0, 3]
>
assoc!
Adds one or more mappings to a Map:
> (setv M (hashmap))
=> {}
> (assoc! M 1 2 3 4 5 6)
=> {1=2, 3=4, 5=6}
Modifying data with non-mutating functions
The mutating functions that were described above have their non-mutating counterparts that have the same names without the '!' suffixes, accept the same arguments and, instead of modifying target objects, return new object with required modifications leaving the original objects unmodified.
For example append
concatenates its argument into new sequence:
> (setv L '(0))
> (append L '(1 2 3) "Hello")
=> [0, 1, 2, 3, H, e, l, l, o]
> L ;;; not changed
=> [0]
>
The returned sequence is of the same kind as the first function argument, so this construction may be used to convert collection into other collection types:
> (append () (hashset 1 2 3))) ;; convert hashset into list
List of non-mutating functions for data modification:
append
, aset
, assoc
, insert
, push
, pop
, put
, put-in
, delete
, reverse
,
sort
Searching and Matching Text Data Using Regular Expressions
Regular expression (regex) support in Explang is implemented using the Java regular expression engine.
This guide assumes that you have general understanding of what the regular expressions are and how to use them. There are good resources for learning about regular expressions and trying them online, for example Regex 101. The reference documentation for Java regex syntax is here: java.util.regex.Pattern.
Creating Regex Patterns
In Explang you can create a regex using a literal syntax. Strings with a '#' in front are interpreted as regexes:
> #"^Hello$"
=> ^Hello$
This will create and instance of java.util.regex.Pattern
that can be
immediately used for matching a text or saved for later use.
Alternatively you can create regexes from string using the re-pattern
function:
> (re-pattern "^Hello$")
=> ^Hello$
The literal syntax is the most convenient because you don't need to double escape regexp special characters. For example, if you want to represent the regex string to match a digit, using a string you would need to write this:
For example, if you want to represent the regex string to match a digit, using a string you would need to write this:
> (re-pattern "\\d")
=> \d
Notice that you have to escape the backslash to get a literal backslash in the string. However with literals you do not need to double escape:
> #"\d"
=> \d
On the other hand, the re-pattern
function is good when you have to
build regexes dynamically or get them from some external source.
Globbing Patterns
Globbing patterns are commonly used to specify sets of filenames with wildcard characters.
*
a wildcard standing for any string of characters?
a wildcard that stands for one character
For example a*.txt
is a glob pattern that would match
names of files with .txt
extension.
Explang supports globbing patterns by internally converting them to regex patterns. One can use the produced pattern just like regular regex patterns.
You can create globbing patterns using a literal syntax, Strings with a '#G' in front are interpreted as regexes:
> #G"a*.txt"
==> a.*\.txt
Alternatively you can create globbing patterns
from strings using the re-glob
function:
> (re-glob "a*.txt")
=> a.*\.txt
Creating a Case Insensitive Regex (and other flags)
The Java regex engine allows to supply extra flags when creating
regexp pattern. The most commonly used flag is i
- case insensitive
matching.
Java regexes allow for a special syntax (embedded flags expression) to enable flags within the regex.
no flags (case-sensitive):
> (re-matches #"abc" "ABC")
=> NIL
case-insensitive flag set
> (re-matches #"(?i)abc" "ABC")
=> ABC
Explang regex literals support specifying flags after the expression:
> (re-matches #"abc"i "ABC" )
=> ABC
The re-pattern
and re-glob
functions accept an optional string
argument that contains string of regexp flags:
> (re-matches (re-pattern "abc" "i") "ABC" )
=> ABC
There are other flags but 'i', see the full list in the Java documentation. The characters used for flags are the same that are used for embedded flags.
Matching a Regex to a String with Groups
Very often, you want to match an entire string. The function to do
that is called re-matches
. re-matches
takes a regex and a
string, then returns the result of the match.
(re-matches <REGEX> <STRING>)
=> <RESULT>
The result it returns is a little complex. There are three things it can return.
If the whole string does not match, re-matches returns NIL.
> (re-matches #"abc" "xyz")
=> NIL
(re-matches #"abc" "zzzabcxxx")
=> NIL
(re-matches #"(a)bc" "hello, world")
=> NIL
If the string does match, and there are no groups (parentheses) in the regex, then it returns the matched string.
> (re-matches #"abc" "abc")
=> abc
> (re-matches #"\d+" "3324")
=> 3324
Note, that can use re-matches as the test expression of a conditional when matched string cannot be empty:
(if (re-matches #"\d+" x)
(println "x is all digits")
(println "x is not all digits"))
Since the implicit Boolean value of an empty string is false, so if regexp matches empty string, you need to explicitly test that the returned value is not null:
(if (notnilp (re-matches #"\d*" x))
(println "x is all digits or empty")
(println "x is not all digits and not empty"))
If it matches and there are groups, then it returns a list, its first element is the entire match. The remaining elements are the group matches.
> (re-matches #"abc(.*)" "abcxyz")
=> ["abcxyz" "xyz"]
> (re-matches #"(a+)(b+)(\d+)" "abb234")
=> ["abb234" "a" "bb" "234"]
Finding a regex substring within a string with groups
Sometimes we want to find a match within a string. re-find
returns the
first match within the string. The return values are similar to
re-matches.
- No match returns nil
> (re-find #"sss" "Loch Ness")
=> NIL
- Match without groups returns the matched string
(re-find #"s+" "dress")
=> "ss"
- Match with groups returns a list
(re-find #"s+(.*)(s+)" "success")
=> ["success" "ucces" "s"]
Finding all substrings that match within a string
To find all substrings that match within a string use re-seq
.
re-seq
returns a lazy seq of all of the matches.
The elements of the seq are whatever type re-find would have returned.
(re-seq #"s+" "mississippi")
=> ("ss" "ss")
(append () (re-seq #"[a-zA-Z](\d+)" "abc x123 b44 234"))
=> [["x123" "123"] ["b44" "44"]]
append ()
materializes lazy sequence that re-seq returns so we can
see text of matched strings.
Compound Expressions
Sometimes it is convenient or necessary to have a single expression that evaluates in order a number of subordinate expressions and returns the value of the last subexpression as value. The previous subexpressions may be run for their side effects or to compute intermediate values that will be used to compute the return value in the last subexpression.
> (setv r (progn
(setl x 1)
(setl y 2)
(println "Computing random value")
(+ x y)))
Computing random value
=> 3
When there are no subordinate expressions NIL
is returned.
> (progn)
=> NIL
Conditionals
If
Operator
The standard conditional operator is if
. Like setv
and defun
, it
does not evaluate all its arguments. When given three arguments, it
evaluates the first, and if that returns true, it returns the value of
the second, otherwise the value of the third:
> (if (> 2 1) 'a 'b)
=> a
> (if (> 1 2) 'a 'b)
=> b
The second argument (then) to if
is a single expression. so if you
want to do there multiple things, combine them into one expression with progn
:
(setv x 70)
(if (>= x 0)
(progn
(println "can compute square root")
(sqrt x))
(println "nope"))
can compute square root
=> 8.366600265340756
If you want several expressions to be evaluated when the condition is false, you can add them as additional arguments:
> (setv x 70)
=> 70
> (if (< x 0)
(println "nope")
(println "can compute square root")
(sqrt x))
can compute square root
=> 8.366600265340756
In fact the third if
argument is optional, if it not provided and the condition does not hold
if
will return null.
(setv x -2)
(if (>= x 0) (sqrt x))
=> NIL
The when
operator
when
is just a simpler form than if
: it has only the 'then' part of it.
This part may contain several expressions. In the case when condition is false
the return value of when is NIL
:
(when (>= x 0)
(println x " is positive")
(println "square root of x is " (sqrt x)))
Implicit Truth values
The if
operator checks the truth value of the first argument. The comparison functions
like <
or >=
return Java Boolean objects true
or false
:
> (< 1 0)
=> false
But, actually, in Explang all expressions have implicit truth values and thus can be used in conditional expressions.
Generally, false
, NIL
, zero numbers of different types,
empty lists, sets, strings and other sequences have implicit False
truth value.
Other objects have truth value of true
.
For example
> (if 0 'yes 'no)
=> no
To check truth value of an expression one can use built-in function bool
:
> (bool "")
=> false
cond
Operator
The cond
conditional operator is used to choose between arbitrary number of alternatives
Syntax for cond
is
(cond ((test1 action1 ...)
(test2 action2 ... )
...
(testn actionn ...)))
Each clause within the cond
statement consists of a conditional test and an list of actions to be performed.
If the first test following cond
, test1, is evaluated to be true, then the related actions are executed,
and value of the its value is returned and the rest of the clauses are skipped over.
If test1 evaluates to be nil, then control moves to the second clause without executing action1, and the same process is followed.
If none of the test conditions are evaluated to be true, then the cond
statement returns nil.
> (setv x 2)
=> 2
> (cond ((< x 0)
(println x " is negative") NIL)
((= x 0)
(println x " is zero") x)
((> x 0)
(println x " is positive") x)
(true (println "This won't happen")))
2 is positive
=> 2
The Logical operators
The logical negation function is called not
, it returns Boolean value, which is inverse of the
the truth value of its argument:
> (not true)
=> false
> (not false)
=> true
> (NOT "foo")
=> false
Logical operators and
and or
:
(or true false)
=> true
They operate like conditionals because they don't evaluate more arguments than they have to and return the last value that was computed
> (and true 0 (println "you'll never see this"))
=> 0
> (or false 0 "foo" (println "you'll never see this too"))
=> foo
Equality Check and Comparison Operators
`=='
The ==
operator compares two objects in sort of
"Do What I Mean" fashion: it returns true if two objects are
equal structurally or numerically:
> (== nil nil)
=> true
> (== 5 5)
=> true
Numerically equal Integer and Long numbers:
> (== 5 5L)
=> true
Numerically equal Integer and Double numbers:
> (== 5 5.0)
=> true
Same inside lists:
> (== '(1.0 2) '(1 2L))
=> true
Numerically unequal Integer and Long numbers:
(== 5 5.1)
=> false
equal
The equal
operator checks two objects for equality by applying the
equals() java method of the first argument on the second one. It will
return true also if both the arguments are NIL
s.
Comparing using java Object's equals method: java.util.List.equals(), which checks that both objects are lists of equal length with equal elements at the same indexes.
> (equal (list 1 2 3) (list 1 2 3))
=> true
> (equal null null)
=> true
> (equal 555 555)
=> true
Different numeric types (Integer and Long)
> (equal 555 555L)
=> false
===
The most strict of the equality checks is the objects identity check
===
: it returns true if Object x is same as Object y or if both
values are NIL. Internally it uses the java operator ==
to check
objects identity.
(=== (list 1 2 3) (list 1 2 3)) ;; check that two variables contain the same object, they are not:
=> false
Note that the ===
operator inherits some quirks from the underlying java operator:
> (=== 5 5)
=> true
> (=== 5555 5555)
=> false
=
Numeric equality operator
The '=' operator checks the arguments for numeric equality taking in account their implicit numeric values. Unlike other equality operators it takes any number of arguments. It returns true if all of them are equal.
> (= 1 1.0)
=> true
> (= 1 1.0 1L 1b 1.0f)
=> true
Implicit numeric values:
> (= nil 0L)
=> true
> (= 1 (list 1))
=> true
> (= 0 '() "")
=> true
> (= 33 "33.0" "33" "33.0f" #\!) ;; ASCII code of ! is 33
=> true
Comparison operators <
, <=
, >=
, >
These operators take any number of arguments and check that all of their arguments are in specified order.
> (< 0 1)
=> true
> (< -1 0 1 2 3)
=> true
> (< 0 1 2 1.5)
=> false
in - test if a sequence contains a value.
If you want to test whether something is one of several alternatives, you could say
(or (== x y) (== x z) ...)
, but this situation is common enough that there's an operator for it.
> (in 3 '(list 1 2 3 "foo" null))
=> true
(in null '(list 1 2 3 "foo" null))
=> true
(in 5 '(list 1 2 3 "foo" null))
=> false
> (in #\o in "hello!")
=> true
Iteration Operators
To iterate through the elements of a list or string, use foreach
.
> (foreach (c (list 1 2 3))
(println c))
1
2
3
=> NIL
The NIL you see at the end is not printed out by the code in the loop. They're the return values of the iteration expressions. One can give one additional argument that will be computed after end of the iteration and used as return value.
> (foreach (c (list 2 3 4) x)
(println c)
(setv x (+ x c)))
=> 9
To continue iterating while some condition is true, use while
.
> (setv x 10)
=> 10
> (while (> x 5)
(setv x (- x 1))
(println x))
9
8
7
6
5
=> 5
The map
function takes a function and a list and returns the result of applying the function to successive elements.
> (map (lambda (x) (+ x 10)) '(1 2 3))
=> [11, 12, 13]
Actually it can take any number of sequences, and keeps going till the shortest runs out:
> (map #'+ '(1 2 3 4) '(100 200 300))
=> [101, 202, 303]
There are functions like map
that apply functions to successive
elements of sequences. The most commonly used is filter
, which
returns the elements satisfying some test.
> (filter (lambda (x) (> x 0)) '(-2 -1 0 1 2 3))
=> [1, 2, 3]
mapprod
returns a sequence consisting of the result of applying
a function to the Cartesian product of the lists:
(mapprod #'str '("A" "B" "C") '("1" "2" "3"))
=> [A1, B1, C1, A2, B2, C2, A3, B3, C3]
reduce
allows to apply function to each sequence element while
accumulating the results. The function must accept two
arguments. Initially it applies the function on the initial value and
the first element of the sequence, then reduce
applies the function
on its previous result and next element in the list. reduce
returns
result of it's last function application.
In this example we compute frequencies table for elements of a list of objects.
> (reduce
(lambda (m x) ; function of m - hashmap, x - next element
(assoc m x (+ 1 (get m x)))) ; increase counter for current word and return updated map
(hashmap) ; initial value of m - empty hashmap of counters
(list 0 1 0 1 2 1 0 1 2 3 2 1 0 1 2 3 4 3 2 1 0))
=> {0=5, 1=7, 2=5, 3=3, 4=1}
Exceptions
When an unexpected condition occurs, a function may be unable to return a reasonable value to its caller. In such case it may have sense for a function to raise an exception that will either terminate the execution of the code or allow exception handling code to handle such a condition.
Throwing Exceptions
In Explang the exception handling mechanism is based on the Java exception
handling and Explang exceptions are Java exception objects (Objects that extend
java.lang.Throwable
).
Exceptions may be thrown implicitly when an unexpected condition occurs in a built-in function or in the language runtime:
> (apply #'unicorn)
EXECUTION ERROR: java.lang.RuntimeException: Symbol unicorn function value is NULL at:
...
> (/ 1 0)
EXECUTION ERROR: java.lang.ArithmeticException: / by zero at:
...
One can throw an exception explicitly from the Explang code using the throw
function:
> (throw "my error")
EXECUTION ERROR: my error at:
The throw
function expects one argument: either an Exception object or a
String
. In the latter case a new io.opsit.explang.ExecutionException
with the given
message will be created and thrown.
To create an exception object without throwing one may uses the exception
function.
The exception may be thrown immediately with the same error as in the code above
> (setl e (exception :message "ssss" ))
=> io.opsit.explang.ExecutionException: ssss
(throw e)
EXECUTION ERROR: ssss at:
Handling of Exceptions
When exception it thrown and not handled the execution of Explang code stops and control is passed to the calling Java code. When working in REPL it will print information about the exception a dump of execution stack and return control to the user, for example, lets define function to compute average of list of numbers.
> (defun average (args)
"Compute average of the numbers in 'args'"
(/ (apply #'+ args)
(length args)))
=> io.opsit.explang.Compiler$LAMBDA$1@2e5d6d97
If we pass an empty list to this function we'll get an arithmetic exception:
> (average ())
EXECUTION ERROR: java.lang.ArithmeticException: / by zero at:
1: / INPUT23:line=4:pos=25:o=123:len=0 Eargs<824318946>
2: average INPUT26:line=1:pos=12:o=12:len=0 Ctx<2128227771>
The try-catch-finally
statement allows to catch the exception and handle it
in user code. For example we can modify the function average
that we used in
one of the previous chapters to handle the exception and return a Not-A-Number
value when called without the arguments:
> (defun average (args)
"Compute average of the numbers in 'args'. Returns a NaN if an error occurs."
(try
(/ (apply #'+ args)
(length args))
(catch Exception ex
(println "Warning: average: " (get ex "message"))
(double "NaN"))))
=> io.opsit.explang.Compiler$LAMBDA$1@6e2c634b
> (average (list 3 5))
=> 4
> (average (list))
Warning: average: / by zero
=> NaN
In the catch
clause we specify the exception class that we want to catch,
variable to put the exception and list of expressions to be evaluated. The
statement supports handling multiple exceptions with multiple catch
blocks. All Java exceptions inherit from the java.lang.Exception
class, so
the above catch
clause would handle any exception that can be thrown inside
the try block. It also means that order of catch blocks is important: first
should be handled more specific exceptions and then less specific according to
the inheritance hierarchy.
In code that works with resources like files or database connections or modifies state of data structures there is often some clean-up tasks that need to be performed after the code has finished its work. Exception handling may complicate performing these tasks because it may cause cause block of code to exit before reaching its normal end.
The finally
block allows to perform some action after all the try
and
catch
code has been evaluated regardless of how it exits. The code of the
finally block is evaluated only for side effects: it does not affect the
return value of the try-catch-finally
statement.
Suppose we want to produce some HTML report on basis of some remote resource. In case of error we want still to output HTML with error messages:
(println "<html><body>")
(try
(let ((data (fetch-data url))
(formatted-data (html-format data)))
(println formatted-data))
(catch IOException ex
(println "ERROR: IO error reading data: "
(get ex "message")))
(catch Exception ex
(println "ERROR: some exception producing report: "
(get ex "message")))
(finally
(println "</body></html>")))
Scope and Binding of Variables
When an Explang program is executing, the variable bindings live in a hierarchy of contexts (scopes). The initial scope, in which the program code starts executing, is called the 'global scope'. Some language constructions introduce their own local scopes.
For example, when a function is called, its arguments receive local values, which are the actual arguments supplied to the function call and those binding have effect only during execution of this function. The context of the code, from which this function has been called, becomes parent context of the newly created scope. Establishing the binding in local context may shadow the bindings in the parent context.
(setv x 1 ; x and y receive values 1 and 2
y 2) ; accordingly in the global context
(defun printit(x) ; x is bound locally and
(list x y)) ; y is used freely inside the function
(printit 3) ; y inside function refers to the global context
;=> (3 2) ; will return list (3 2)
x ; will still return 1
(setv y 3) ; now we change the y value to 3 in the global
(printit 3) ; now y inside the function refers to the new value
;=> (3 3) ; will return list (3 3)
The function printit
refers to y
. This is a free reference in the sense that
there is no binding to y inside the function. When we refer to y
inside the
function it refers to the binding in the parent context. On the other hand x
Such type of variable binding is called Dynamic Binding. When variable is dynamically bound, its current binding is simply the most recently created local binding for that name. Or the global binding if there is no such local binding.
LET
construction
let
is another construction that introduces local scope of execution. It receives
list of variable bindings that will be established in the newly created scope:
(setv x -99) ; x receives an initial value of -99
(defun incx ()
(setv x (+ 1 x))) ; increment x and return its value
(let ((x 1) (y 10)) ; x is bound to 1 locally, y to 10
(incx) ; executes inside newly created context
(incx) ;
(list x y))
;=> (3 10) ; will return list (3 10)
(incx) ; will return -98
x ; x now is -98
y ; will fail because let
; did not establish global binding for y
SETL
, SETV
and SETQ
constructions
The SETL
, SETV
and SETQ
construction all serve to establish variable
bindings. They all get list of pairs { variable value } and establish binding
for these variables. The difference in their handling of variable scopes.
SETL
(set local) always sets binding in the local context. If there is an
variable with same name in one of parent contexts it will be shadowed.
> (setl x 1) ; set value of x in the global context
> (let ((z 0)) ; introduce local context with z = 0
(setl x 2 y 3 z 4); set values in local context
(list x y z))
;=> (2 3 4)
> x ; x is still 1 in the global context
;=> 1
> y ; will return error, y is unbound in the global context
> z ; will return error, z is unbound in the global context
SETV
on the other hand will change variable value if it exists in parent
contexts. It will start looking for the variable binding in the local context
and continue looking up in the contexts hierarchy. If it finds such a binding
it will change it to the new value.If no such binding exists it will establish
a new binding in the local context.
> (setv x 1) ; set value of x in the global context
> (let ((z 0)) ; introduce local context with z = 0
(setv x 2 y 3 z 4); set x in the global, y and z in the local context
(list x y 4))
;=> (2 3 4)
> x ; x is changed to 2 in the global context
;=> 2
> y ; will return error, y is unbound in the global context
> z ; will return error, z is unbound in the global context
SETQ
will look for an existing binding and update it in the same way as SETV
.
But in case when no such binding exists it will establish new binding in the
initial context.
> (setq x 1) ; set value of x in the global context
> (let ((z 0)) ; introduce local context
(setq x 2 y 3 z 4); set x, y in the global and z in the local context
(list x y z))
;=> (2 3 4)
> x ; x is changed to 2 in the global context
;=> 2
> y ; y is now bound to 3 in the global context
;=> 3
> z ; will return error, z is unbound in the global context
In general, when writing generally useful functions, one would want to use
LET
or SETL
to declare local variables and avoid modifying variables with
same name in outer scopes.
On the other hand, when writing specialized functions that will be run in execution contexts that are known beforehand, ability to access and modify global variables from inside functions allows to avoid passing lots of variables or complex data structures as function arguments.