Skip to content

Working with spaces

Space API

Spaces can have different implementations, but should satisfy a certain API. This API includes pattern-matching (or unification) functionality. match is an stdlib function, which calls a corresponding API function of the given space, which can be different from the program space.

Let us recap that the type of match is (-> hyperon::space::DynSpace Atom Atom %Undefined%). The first argument is a space (or, more precisely, a grounded atom referring to a space) satisfying the Space API. The second argument is the input pattern to be unified with expressions in the space, and the third argument is the output pattern, which is instantiated for every found match. match can produce any number of results starting with zero, which are treated nondeterministically.

The basic use of match was already covered before, while its use with custom spaces will be described in other tutorials, since these spaces are not the part of stdlib. However, the Space API includes additional components, which are utilized by such stdlib functions as add-atom and remove-atom.

Adding atoms

The content of spaces can be not only defined statically in MeTTa scripts, but can also be modified at runtime by programs residing in the same or other spaces.

The function add-atom adds an atom into the Space. Its type is (-> hyperon::space::DynSpace Atom (->)). The first argument is an atom referring some Space, to which an atom provided as the second argument will be added. Since the type of the second argument is Atom, the added atom is added as is without reduction.

In the following program, add-foo-eq is a function, which adds an equality for foo to the program space whenever called. Then, it is checked that the expressions are added to the space without reduction.

metta
(: add-foo-eq (-> Atom (->)))
(= (add-foo-eq $x)
   (add-atom &self (= (foo) $x)))
! (foo) ; (foo) - not reduced
! (add-foo-eq (+ 1 2)) ; () - OK
! (add-foo-eq (+ 3 4)) ; () - OK
! (foo) ; [3, 7]
! (match &self (= (foo) $x)
    (quote $x)) ; [(quote (+ 1 2)), (quote (+ 3 4))]

If it is desirable to add a reduced atom without additional wrappers (e.g., like add-foo-eq but without Atom type for the argument), then add-reduct can be used:

metta
! (add-reduct &self (= (foo) (+ 3 4))) ; ()
! (foo) ; 7
! (match &self (= (foo) $x)
    (quote $x)) ; (quote 7)

Removing atoms

The function remove-atom removes an atom from the AtomSpace without reducing it. Its type is (-> hyperon::space::DynSpace Atom (->)).

The first argument is a reference to the space from which the Atom needs to be removed, the second is the atom to be removed. Notice that if the given atom is not in the space, remove-atom currently neither raises a error nor returns the empty result.

metta
(Atom to remove)
! (match &self (Atom to remove) "Atom exists") ; "Atom exists"
! (remove-atom &self (Atom to remove)) ; ()
! (match &self (Atom to remove) "Unexpected") ; nothing
! (remove-atom &self (Atom to remove)) ; ()

Combination of remove-atom and add-atom can be used for graph rewriting. Consider the following example.

metta
(link A B)
(link B C)
(link C A)
(link C E)

! (match &self (, (link $x $y)
                  (link $y $z)
                  (link $z $x))
               (let () (remove-atom &self (link $x $y))
                       (add-atom &self (link $y $x)))
  ) ; [(), (), ()]
! (match &self (link $x $y)
    (link $x $y)) ; [(link A C), (link C B), (link B A), (link C E)]

Here, we find entries (link _ _), which form three-element loops, and revert the direction of links in them. Let us note that match returns three unit results, because the loop can start from any of such entries. All of them are reverted (only (link C E) remains unchanged). Also, in the current implementation, match first finds all the matches, and then instantiates the output pattern with them, which is evaluated outside match. If remove-atom and add-atom would be executed right away for each found matching, the condition of circular links would be broken after the first rewrite. This behavior can be space-specific, and is not a part of MeTTa specification at the moment. This can be changed in the future.

New spaces

It is possible to create other spaces with the use of new-space function from stdlib. Its type is (-> hyperon::space::DynSpace), so it has no arguments and returns a fresh space. Creating new spaces can be useful to keep the program space cleaner, or to simplify queries.

If we just run (new-space) like this

metta
! (new-space)

we will get something like GroundingSpace-0x10703b398 as a textual representation space atom. But how can we refer to this space in other parts of the program? Notice that the following code will not work as desired

metta
(= (get-space) (new-space))
! (add-atom (get-space) (Parent Bob Ann)) ; ()
! (match (get-space) (Parent $x $y) ($x $y)) ; nothing

because (get-space) will create a brand new space each time.

One workaround for this issue in a functional programming style is to wrap the whole program into a function, which accepts a space as an input and passes it to subfunctions, which need it:

metta
(= (main $space)
   (let () (add-atom $space (Parent Bob Ann))
     (match $space (Parent $x $y) ($x $y))
   )
)
! (main (new-space)) ; (Bob Ann)

This approach has its own merits. However, a more direct fix for (= (get-space) (new-space)) would be just to evaluate (new-space) before adding it to the program:

metta
! (add-reduct &self (= (get-space) (new-space))) ; ()
! (add-atom (get-space) (Parent Bob Ann)) ; ()
! (get-space) ; GroundingSpace-addr
! (match (get-space) (Parent $x $y) ($x $y)) ; (Bob Ann)

That is, (new-space) is evaluated to a grounded atom, which wraps a newly created space. Other elements of (= (get-space) (new-space)) are not reduced. Instead of add-reduct, one could use the following more explicit code

metta
! (let $space (new-space)
    (add-atom &self (= (get-space) $space)))

which also ensured that nothing is reduced except (new-space).

Creating tokens

Why can't we refer to the grounded atom, which wraps the created space? Indeed, we can represent such grounded atoms as numbers or operations over them in the code. And what is about &self?

In fact, they are turned into atoms from their textual representation by the parser, which knows a mapping from textual tokens (defined with the use of regular expressions) to constructors of corresponding grounded atom. Basically, &self is replaced with the grounded atom wrapping the program space by the parser before it gets inside the interpreter.

Parsing is explained in more detail in another tutorial, while here we focus on the stdlib function bind!.

bind! registers a new token which is replaced with an atom during the parsing of the rest of the program. Its type is (-> Symbol %Undefined% (->)).

The first argument has type Symbol, so technically we can use any valid symbol as the token name, but conventionally the token should start with &, when it is bound to a custom grounded atom, to distinguish it from symbols. The second argument is the atom, which is associated with the token after reduction. This atom should not necessarily be a grounded atom. bind! returns the unit value () similar to println! or add-atom.

Consider the following program:

metta
(= (get-hello) &hello)
! (bind! &hello (Hello world)) ; ()
! (get-metatype &hello) ; Expression
! &hello ; (Hello world)
! (get-hello) ; &hello

We first define the function (get-hello), which returns the symbol &hello. Then, we bind the token &hello to the atom (Hello world). Note that the metatype of &hello is Expression, because it is replaced by the parser and gets to the interpreter already as (Hello world). ! &hello is expectedly (Hello world). Once again, &hello is not reduced to (Hello world) by the interpreter. It is replaced with it by the parser. It can be seen by the fact that (get-hello) returns &hello as a symbol, because it was parsed and added to the program space before bind!.

bind! might be tempting to use to refer to some lengthy constant expressions, e.g.

metta
! (bind! &x (foo1 (foo2 3) 45 (A (v))))
! &x

However, this lengthy expression will be inserted to the program in place of every occurrence of &x. However, let us note again that the second argument of bind! is evaluated before bind! is called, which is especially important with functions with side effects. For example, the following program will print "test" only once, while &res will be simply replaced with ().

metta
! (bind! &res (println! "test"))
! &res
! &res

Using bind! for unique grounded atoms intensively used in the program can be more reasonable. Binding spaces created with (new-space) to tokens is one of possible use cases:

metta
! (bind! &space (new-space)) ; ()
! (add-atom &space (Parent Bob Ann)) ; ()
! &space ; GroundingSpace-addr
! (match &space (Parent $x $y) ($x $y)) ; (Bob Ann)
! (match &self (Parent $x $y) ($x $y)) ; empty

However, if spaces are created dynamically depending on runtime data, bind! is not usable.

Imports

Stdlib has operations for importing scripts and modules. One such operation is import!. It accepts two arguments. The first argument is a symbol, which is turned into the token for accessing the imported module. The second argument is the module name. For example, the program from the tutorial could be split into two scripts - one containing knowledge, and another one querying it.

metta
; people_kb.metta
(Female Pam)
(Male Tom)
(Male Bob)
(Female Liz)
(Female Pat)
(Female Ann)
(Male Jim)
(Parent Tom Bob)
(Parent Pam Bob)
(Parent Tom Liz)
(Parent Bob Ann)
(Parent Bob Pat)
(Parent Pat Jim)
metta
; main.metta
! (import! &people people_kb)
(= (get-sister $x)
   (match &people
     (, (Parent $y $x)
        (Parent $y $z)
        (Female $z))
     $z
   )
)
! (get-sister Bob)

Here, (import! &people people_kb) looks similar to (bind! &people (new-space)), but import! fills in the loaded space with atoms from the script. Let us note that import! does more work than just loading the script into a space. It interacts with the module system, which is described in another tutorial.

&self can be passed as the first argument to import!. In this case, the script or module will still be loaded into a separate space, but the atom wrapping this space will be inserted to &self. Pattern matching queries encountering such atoms will delegate queries to them (with the exception, when the space atom itself matches against the query, which happens, when this query is just a variable, e.g., $x). Thus, it works similar to inserting all the atoms to &self, but with some differences, when importing the same module happens multiple times, say, in different submodules.

One may use get-atoms method to see that the empty MeTTa script is not that empty and contains the stdlib space(s). Note that the result get-atoms will be reduced. Thus, it is not recommended to use in general.

metta
! (get-atoms &self)

Some space atoms are present in the seemingly empty program since some modules are pre-imported. Indeed, one can find, say, if definition in &self, which actually resides in the stdlib space inserted into &self as an atom

metta
! (match &self
          (= (if $cond $then $else) $result)
   (quote (= (if $cond $then $else) $result))
  )

mod-space! returns the space of the module (and tries to load the module if it is not loaded into the module system). Thus, we can explore the module space explicitly.

metta
! (mod-space! stdlib)
! (match (mod-space! stdlib)
          (= (if $cond $then $else) $result)
   (quote (= (if $cond $then $else) $result))
  )