Appearance
Recursion and control
Basic recursion
A natural way to represent repetitive computations in MeTTa is recursion like in traditional functional languages, especially for processing recursive data structures. Let us consider a very basic recursive function, which calculates the number of elements in the list.
metta
(= (length ()) 0)
(= (length (:: $x $xs))
(+ 1 (length $xs)))
! (length (:: A (:: B (:: C ()))))
The
function has two cases, which are mutually exclusive de facto, and act
as a conditional control structure. The base case returns 0
for an empty list ()
. Recursion itself takes place inside the second equality, in which length
is defined via itself on the deconstructed parameter.
Notice that we didn't define the recursive data structure (list) here, and used arbitrary atoms (::
and ()
) as data constructors. length
can be called on anything, e.g. (length (hello world))
,
but this expression will simply be not reduced, because there are no
suitable equalities for it. You can write your own version of length for
other Cons
and Nil
instead of ::
and ()
:
metta sandbox
metta
(= (length ...) 0)
(= (length ...)
(+ 1 (length $xs)))
! (length (Cons A (Cons B (Cons C Nil))))
If a function expects specific subset of all possible expressions as input, types for corresponding atoms should be defined. However, we focus here on the basic evaluation process itself and leave types for another tutorial.
Higher order functions
Higher-order functions is a powerful abstraction, which naturally appears in MeTTa. Consider the following code:
metta
(= (apply-twice $f $x)
($f ($f $x)))
(= (square $x) (* $x $x))
(= (duplicate $x) ($x $x))
! (apply-twice square 2) ; 16
! (apply-twice duplicate 2) ; ((2 2) (2 2))
! (apply-twice 1 2) ; (1 (1 2))
apply-twice
takes a function as its first parameter and applies it twice to its
second parameter. In fact, it doesn't really care if it is a function or
not. It simply constructs a corresponding expression for further
evaluation.
Passing functions into recursive functions is very convenient for processing various collections. Consider the following basic example
metta
(= (map $f ()) ())
(= (map $f (:: $x $xs))
(:: ($f $x) (map $f $xs)))
(= (square $x) (* $x $x))
(= (twice $x) (* $x 2))
! (map square (:: 1 (:: 2 (:: 3 ())))) ; (:: 1 (:: 4 (:: 9 ())))
! (map twice (:: 1 (:: 2 (:: 3 ())))) ; (:: 2 (:: 4 (:: 6 ())))
! (map A (:: 1 (:: 2 (:: 3 ())))) ; (:: (A 1) (:: (A 2) (:: (A 3) ())))
map
transforms a list by applying a given function (or constructor) to each
element. There is a rich toolset of higher-order functions in
functional programming. They are covered in another tutorial.
Conditional statements
Let
us imagine that we want to implement the factorial operation. If we
want to use grounded arithmetics, we will not be able to use pattern
matching to deconstruct a grounded number and distinguish the base and
recursive cases. We can write (= (fact 0) 1)
, but we cannot just write (= (fact $x) (* $x (fact (- $x 1))))
. However, we can use if
, which works much like if-then-else construction in any other language. Consider the following code
metta
(= (factorial $x)
(if (> $x 0)
(* $x (factorial (- $x 1)))
1))
! (factorial 5) ; 120
(factorial $x)
will be reduced to (* $x (factorial (- $x 1)))
if (> $x 0)
is True
, and to 1
otherwise.
It should be noted that if
doesn't evaluate all its arguments, but "then" and "else" branches are evaluated only when needed. factorial
wouldn't work otherwise, although this should be more obvious from the following code, which will not execute the infinite loop
metta
(= (loop) (loop)) ; this is an infinite loop
! (if True Success (loop)) ; Success
Application of if
looks like as an ordinary function application, and if
is indeed implemented in pure MeTTa as a function. How it is done is discussed in another tutorial.
Another conditional statement in MeTTa is case
,
which pattern-matches the given atom against a number of patterns
sequentially in a mutually exclusive way. A different version of the
factorial operation can be implemented with it:
metta
(= (factorial $x)
(case $x
((0 1)
($_ (* $x (factorial (- $x 1)))))
)
)
! (factorial 5) ; 120
In contrast to if
, case
doesn't check logical conditions but performs pattern matching similar
to application of a function with several equality definitions. Thus,
their usage is somewhat different. For example, if one wants to zip two
lists, it is convenient to distinguish two cases - when both lists are
empty, and both lists are not empty. But when two lists are of different
lengths, there will a situation when neither of these cases will be
applicable, and the expression will not be reduced. Try to run this
code:
metta sandbox
metta
(= (zip () ()) ())
(= (zip (:: $x $xs) (:: $y $ys))
(:: ($x $y) (zip $xs $ys)))
! (zip (:: A (:: B ())) (:: 1 (:: 2 ()))) ; (:: (A 1) (:: (B 2) ()))
! (zip (:: A (:: B ())) (:: 1 ())) ; (:: (A 1) (zip (:: B ()) ()))
The non-matchable part remains unreduced. Of course, adding two equalities for (zip (:: $x $xs) ())
and (zip () (:: $y $ys))
could be used (you can try to add them in the above code), and it would be a more preferable way in some cases. However, using case
here could be more convenient:
metta
(= (zip $list1 $list2)
(case ($list1 $list2)
(((() ()) ())
(((:: $x $xs) (:: $y $ys)) (:: ($x $y) (zip $xs $ys)))
($else ERROR)
)
)
)
! (zip (:: A (:: B ())) (:: 1 (:: 2 ()))) ; (:: (A 1) (:: (B 2) ()))
! (zip (:: A (:: B ())) (:: 1 ())) ; (:: (A 1) ERROR)