Appearance
Operations over atoms
Stdlib contains operations to construct and deconstruct atoms as instances of Expression
meta-type. Let us first describe these operations.
Deconstructing expressions
car-atom
and cdr-atom
are fundamental operations that are used to manipulate atoms. They are
named after 'car' and 'cdr' operations in Lisp and other similar
programming languages.
The car-atom
function extracts the first atom of an expression as a tuple.
metta
! (get-type car-atom) ; (-> Expression %Undefined%)
! (car-atom (1 2 3)) ; 1
! (car-atom (Cons X Nil)) ; Cons
! (car-atom (seg (point 1 1) (point 1 4))) ; seg
The cdr-atom
function extracts the tail of an expression, that is, all the atoms of the argument except the first one.
metta
! (get-type cdr-atom) ; (-> Expression %Undefined%)
! (cdr-atom (1 2 3)) ; (2 3)
! (cdr-atom (Cons X Nil)) ; (X Nil)
! (cdr-atom (seg (point 1 1) (point 1 4))) ; ((point 1 1) (point 1 4))
Constructing expressions
cons-atom
is a function, which constructs an expression using two arguments, the
first of which serves as a head and the second serves as a tail.
metta
! (get-type cons-atom) ; (-> Atom Expression Expression)
! (cons-atom 1 (2 3)) ; (1 2 3)
! (cons-atom Cons (X Nil)) ; (Cons X Nil)
! (cons-atom seg ((point 1 1) (point 1 4))) ; (seg (point 1 1) (point 1 4))
cons-atom
reverses the results of car-atom
and cdr-atom
:
metta
(= (reconstruct $xs)
(let* (($head (car-atom $xs))
($tail (cdr-atom $xs)))
(cons-atom $head $tail))
)
! (reconstruct (1 2 3)) ; (1 2 3)
! (reconstruct (Cons X Nil)) ; (Cons X Nil)
Note that we need let
in the code above, because cons-atom
expects "meta-typed" arguments, which are not reduced. For example, cdr-atom
will not be evaluated in the following code:
metta
! (cons-atom 1 (cdr-atom (1 2 3))) ; (1 cdr-atom (1 2 3))
Let us consider how basic recursive processing of expressions can be implemented:
metta
(: map-expr (-> (-> $t $t) Expression Expression))
(= (map-expr $f $expr)
(if (== $expr ()) ()
(let* (($head (car-atom $expr))
($tail (cdr-atom $expr))
($head-new ($f $head))
($tail-new (map-expr $f $tail))
)
(cons-atom $head-new $tail-new)
)
)
)
! (map-expr not (False True False False))
Comparison with custom data constructors
A typical way to construct lists using custom data structures is to introduce a symbol, which can be used for pattern-matching. Then, extracting heads and tails of lists becomes straightforward, and special functions for this are not need. They can be easily implemented via pattern-matching:
metta
(= (car (Cons $x $xs)) $x)
(= (cdr (Cons $x $xs)) $xs)
! (cdr (Cons 1 (Cons 2 (Cons 3 Nil))))
But one can implement recursive processing without car
and cons
:
metta
(: map (-> (-> $t $t) Expression Expression))
(= (map $f Nil) Nil)
(= (map $f (Cons $x $xs))
(Cons ($f $x) (map $f $xs)))
! (map not (Cons False (Cons True (Cons False (Cons False Nil)))))
Instead of Expression
, one would typically use a polymorphic List
type (as described another tutorial).
Implementing map
with the use of pattern matching over list constructors is much simpler. Why can't it be made with cons-atom
? cons-atom
, car-atom
, cdr-atom
work on the very base meta-level as grounded functions. If we
introduced explicit constructors for expressions, then we would just
move this meta-level further, and the question would arise how
expressions with these new constructors are constructed. Apparently, we
need to stop somewhere and introduce the very basic operations to
construct all other composite expressions. Using explicit data
constructors should typically be preferred over resorting to these
atom-level operations.
Typical usage
car-atom
and cdr-atom
are typically used for recursive traversal of an expression. One basic
example is creation of lists from tuples. In case of reducible
non-nested lists, the code is simple:
metta
(= (to-list $expr)
(if (== $expr ()) Nil
(Cons (car-atom $expr)
(to-list (cdr-atom $expr)))
)
)
! (to-list (False (True False) False False))
Parsing
a tuple of arbitrary length (if the use of explicit constructors is not
convenient) is a good use case for operations with expressions. For
example, one may try implementing let*
by subsequently processing the tuple of variable-value pairs and applying let
.
One
more fundamental use case for analyzing expressions is implementation
of custom interpretation schemes, if they go beyond the default MeTTa
interpretation process and domain specific languages. A separate
tutorial will be devoted to this topic. But let us note here that
combining car-atom
and cdr-atom
with get-metatype
will be a typical pattern here. Here, we provide a simple example for parsing nested tuples:
metta
(= (to-tree $expr)
(case (get-metatype $expr)
((Expression
(if (== $expr ()) Nil
(Cons (to-tree (car-atom $expr))
(to-tree (cdr-atom $expr)))
))
($_ $expr)
)
)
)
! (to-tree (False (True False) False False))
Note the difference of the result with to-list
. The internal (True False)
is also converted to the list now. It happens because the head of the current tuple is also passed to to-tree
. For this to work, we need to analyze if the argument is an expression. If it is not, the value is not transformed.