TXR Lisp is an innovative, original dialect from the Lisp language family. It is not an implementation of Common Lisp, Scheme or any existing dialect.
One goals of TXR Lisp is to remove verbosity from Lisp programming, where doing so does not conflict with the Lisp ideology ("Lispiness"). This means that the primary means of making programs succinct are semantic devices, rather than syntactic sugars. However, sugars are also employed, when they are not disruptive to the surrounding syntax.
Another goal of TXR Lisp is to serve as a platform for
experimentation an advancement within the context of a Lisp paradigm,
and bring together the right mix of advanced features as well as
innovative new solutions to old problems, integrating these into an ergonomic whole
which can be applied to real, practical tasks, right now.
TXR Lisp supports functional programming, but isn't a functional language. The TXR project consciously rejects the purely functional ideology. TXR Lisp supports programming with higher order functions by providing
TXR Lisp supports imperative programming directly. It has mutable
variables, and mutable data structures such as lists, vectors, strings
and structures. It has control constructs such as loops, exception
handling and non-local exits.
TXR Lisp is strictly evaluated. Function argument expressions are evaluated in left to right order, before the function call takes place. Special operators and macros evaluate forms in a predictable order. Sequencing operations with visible effects such as I/O is easy, just like in any mainstream imperative language. TXR Lisp, however, has support for lazy evaluation via several features:
delay
and force
operators;
andmlet
, which is
an advancement over Scheme's letrec
.TXR Lisp's construct for explicit laziness in the middle of a strict language provide most of the benefits of lazy evaluation, without the drawbacks such as poor performance, confusing semantics and clumsy coordination of side effects which plague lazily-evaluated functional languages.
Unlike in mainstream Lisp dialects, TXR Lisp allows traditional list
operations like car
, cdr
, and mapcar
to be applied to vectors and character strings, which is very
expressive and useful.
A classic dilemma in the design of a Lisp dialect is whether to make
it Lisp-1 or Lisp-2. That is to say, should function and variable
bindings be in one namespace or in separate namespaces? TXR Lisp
innovates in this area with a new solution which integrates both
styles. The underlying infracstructure is Lisp-2: the global and
lexical environments have function and variable namespaces. However,
Lisp-1 style evaluation of arguments (with the two namespaces
apparently folded into one) occurs in forms which are written using
square brackets instead of parentheses. This feature is deeply
integrated into the language; it cannot be implemented in Lisp-2
dialects to the same level without working at the implemenation level,
or else transforming entire top-level forms with a code walker, or else
making it an incomplete hack. Even the macro expander is aware of the
feature: when a reference to a lexical function occurs as a Lisp-1
style argument, that function shadows a symbol macro in an outer scope,
which would not be shadowed in a Lisp-2 form due to the symbol macro
being considered in a variable namespace.
Programming with functional arguments ("higher order functions") in
TXR Lisp is free of distracting noise like funcall
and #'
.
The funcall
function exists and is named call
,
and the function operator is called fun
; but these are
hardly ever seen. The Common Lisp #'
(hash quote)
notation is absent. The lambda
operator in TXR
Lisp works directly; it isn't a macro which expands
to (fun (lambda ...))
. Yet, TXR Lisp
retains the advantages of Lisp-2, such as its natural view of macro
programming and referential hygiene.
Incidentally, the square bracket forms in TXR Lisp provide a purer
vision of Lisp-1 than Lisp-1 dialects themselves. Proponents of Lisp-1
dialects like to say that every position of a form is
evaluated in the same way, which is more consistent than Lisp-2 which
treats the operator specially. Unfortunately, that is a lie, because
any worthwhile Lisp-1 dialect has macros. The leftmost position of a
form in any worthwhile Lisp-1 dialect must be considered in the macro
namespace. By contrast, TXR Lisp's square
bracket forms cannot be macro forms. In the form [a b c]
,
the symbol a
must resolve to a function, or an
object which can be used as a function. It cannot be a macro (other
than a symbol macro which simply replaces a
with another
form). So the Lisp-1 advocacy soundbite is, ironically, true in TXR
Lisp: the arguments of a square bracketed form are all evaluated the
same way, period.
Programmers who encounter one of the major Lisp dialects for the first time usually complain about the clumsy support for working with arrays. TXR Lisp takes heed of these complains. TXR's square bracket forms provide array indexing and range referencing.
Indexing works naturally because sequences (lists, vectors and
arrays) as well as hashes are considered functions which map indexes to
elements. For instance, the form (mapcar "abc" #(2 0 1))
produces the vector #(#\c #\a #\b)
because the character
string "abc"
is a function which maps the indices 0, 1
and 2 to the character objects #\a
, #\b
and
#\c
. (Also note that a vector is being processed with mapcar
,
which is why the type which emerges is a vector). Therefore, the
square brackets Lisp-1 notation provides array indexing. The form ["abc"
1]
means "call the "abc"
string as a
function, passing it the argument 1". The effect is that the character #\b
is retrieved. This works for lists and vectors in the same way.
For hash tables, a hash lookup is performed. If h
is a
hash table, and k
is a key, then [h k]
performs a lookup. Also, [h k v]
performs a lookup such
that v
is substituted if k
is not found.
Range indexing is supported using the dotdot range notation. For
instance [a 2..5]
denotes the slice of a starting from
element [a 2]
up to and including [a 4]
,
excluding [a 5]
. If the symbol t
is
used as the upper endpoint, it denotes the element one beyond the last;
in other words, the slice extends to the end. The colon symbol : can
also be used on either end to denote "from the start" or "to the end".
All of the following forms denote a slice of a a
which
includes all of a
: [a :..:]
, [a
0..:]
, [a 0..t]
, [a :..t]
.
Negative indices are supported, so that -1 denotes the last element.
The expression [a 0..-1]
calculates a slice of a which
excludes the last element. The dotdot notation is a syntactic
sugar, which denotes the construction of a range object: a..b
is converted by TXR Lisp's parser to (rcons a b)
, a call
to the rcons
function which constructs a range object.
Both element and range indexing forms support assignment as well as
deletion (if the array-like object is stored in an assignable place),
which makes for flexible and succinct array editing. For instance, if
variable a
holds the string "car"
, then (set
[a 1..2] "ape")
changes a
to "caper"
.
The following example shows how we can exchange the ranges of two
arrays in a single swap
operation:
(let ((a "archibald") (b "spooner"))
(swap [a 2..4] [b 0..2])
(list a b))
-> ("arspibald" "chooner")
Note that although the exchanged ranges happen to be of equal length in the example, that isn't a constraint.
Direct interpolation of values into strings is a considerable convenience in programming languages. Their use leads to succinct, expressive code for string construction. Mainstream Lisp dialects are missing the boat in this regard. TXR Lisp has better designed string interpolation than most scripting languages.
Interpolated string literals are called "quasiliterals" in TXR Lisp, and are delimited by backquotes rather than double quotes. TXR Lisp avoids the mistaken design of featuring just one kind of string literal, which supports interpolation. There is a need for strings which are true literals.
In a backquoted string, the @
character denotes the
insertion of the value of an expression. It is followed by an
expression directly, or an expression surrounded in curly braces.
The curly brace notation solves certain ambiguity problems which arise,
and also allows for the expression of modifiers for expressing field
width, left or right alignment and a separator string for merging list
elements. Example:
(defvarl str "abc")
(defvarl words '#"how now brown cow")
(prinl `@str-@str`)
(prinl `@{str 10}-@{str -10}`)
(prinl `Words: @{words ", " -40}`)
Output:
"abc-abc"
"abc - abc"
"Words: how, now, brown, cow"
Both quasiliterals and regular string literals can be prefixed by #
which denotes a word list. In the above example, #"how now brown cow"
denotes the list structure ("how" "now" "brown" "cow").
This is prefixed with a quote to express the quoted list '("how" "now" "brown" "cow")
.
The ommission of the quote is necessary because word lists can be
embedded in unevaluated structure. However, quasiliterals are
structures intended for evaluation and so the expression #`how now brown cow`
evaluates to the list ("how" "now" "brown" "cow")
without the need for a quote. Quasiliteral syntax produces code
which, when evaluated, constructs the implied character string or list
of strings, whereas an ordinary literal denotes a string or list of
strings as program syntax.
TXR Lisp doesn't have an object system similar to Common Lisp's CLOS. Rather, TXR Lisp's structures make up a simple object system featuring single dispatch and multiple inheritance.
The term "class" is avoided in TXR Lisp; rather it has
structures (structs) which have object-oriented features.
Struct types must be explicitly defined using the
defstruct
macro, or else the underlying API that it uses.
Structs have both instance slots and static slots. Under inheritance,
a static slot can be overriden with an instance slot or vice versa.
Methods are represented as function values stored in static slots,
and therefore methods "belong" to structs. Dispatch of a method named m
on an object instance o
takes place by dispatching the function
stored in the slot named m
of o
. The function
receives the object as its leftmost argument, followed by the remaining
method arguments. New static slots can be added to an existing type. Also, a
type which has an inherited static slot can break that relationship and get its
own non-inherited instance of the static slot. This is the basis for method
definitions and redefinitions outside of defstruct
.
TXR Lisp features a dot notation for referring to struct slots, including
method slots. For instance sim.start-time.(set 42)
means
to retrieve the slot start-time
of the sim
structure, and then invoke the set
method on that
start-time
. This notation is a syntactic sugar for the Lisp
syntax (qref sim start-time (set 42))
. The qref
symbol has a macro binding; that macro compiles the abstract syntax into
the slot references and function calls that it denotes.
Suppose we have a list of objects, and we'd like to call set
on their respective start-time
members to reset their
times to zero:
(mapdo .start-time.(set 42) obj-list)
Here, the dot notation begins with a leading dot. This variant of the notation
doesn't correspond to qref
but to uref
(unbound reference):
(uref start-time (set 42))
. This uref
form compiles
into a higher order function which takes an object as an argument. That function
references the object's start-time
slot and calls the set
method on it.
Objects have methods called on construction,
as well as finalizers. When the garbage collector detects an unreachable object
whose finalizers have not yet been called, the finalizers are called at that
time. An object's finalizers are also called if an exception occurs during its
construction, and may also be called explicitly before the object's lifetime
ends via the call-finalizers
function. The
with-objects
macro instantiates objects in a lexical scope, and
calls their finalizers when their scope ends, enabling some aspects of the
"RAII" idiom from the C++ language to be used in TXR Lisp.