Skip to content

Handling nondeterministic results

Superpose

In previous tutorials we saw that match along with any other function can return multiple (nondeterministic) as well as empty results. If you need to get a nondeterministic result explicitly, use the superpose function, which turns a tuple into a nondeterministic result. It is an stdlib function of (-> Expression Atom) type.

However, it is typically recommended to avoid using it. For example, in the following program

metta
(= (bin) 0)
(= (bin) 1)
(= (bin2) (superpose (0 1)))
! (bin) ; [0, 1]
! (bin2) ; [0, 1]

bin and bin2 do similar job. However, bin is evaluated using one equality query, while bin2 requires additional evaluation of superpose. Also, one may argue that bin is more modular and more suitable for meta-programming and evaluation control.

One may want to use superpose to execute several operations. However, the order of execution is not guaranteed. And again, one can try thinking about writing multiple equalities for a function, inside which superpose seems to be suitable.

However, superpose can still be convenient in some cases. For example, one can pass nondeterministic expressions to any function (both grounded and symbolic, built-in and custom) and get multiple results. In the following example, writing a nondeterministic function returning 3, 4, 5 would be inconvenient:

metta
! (+ 2 (superpose (3 4 5))) ; [5, 6, 7]

Here, nondeterminism works like a map over a set of elements.

Another example, where using superpose explicitly is useful is for checking a set of nondeterministic results with assertEqual, when both arguments still require evaluation (so assertEqualToResult is not convenient to apply). In the following example, we want to check that we didn't forget any equality for (color), but we may not be interested what exact value they are reduced to (i.e., whether (ikb) is reduced to international-klein-blue or something else).

metta
(= (ikb) international-klein-blue)
(= (color) green)
(= (color) yellow)
(= (color) (ikb))

!(assertEqual
  (match &self (= (color) $x) $x)
  (superpose ((ikb) yellow green))) ; ()
!(assertEqualToResult
   (match &self (= (color) $x) $x)
  ((ikb) yellow green)) ; Error

Empty

As mentioned above, in MeTTa, functions can return empty results. This is a natural consequence on the evaluation semantics based on queries, which can find no matches. Sometimes, we may want to force a function to "return" an empty result to abort a certain evaluation branch, or to explicitly represent it to analyze this behavior on the meta-level.

(superpose ()) will exactly return the empty set of results. However, stdlib provide (empty) function to do the same in a clearer and stable way. Some versions may also use Empty as a symbol to inform the interpreter about the empty result, which may differ on some level from calling a grounded function, which really returns an empty set. (empty) is supported more widely at the moment, so we use it here.

(empty) could be useful in the construction of the asserts (assertEqual (...) (empty)), but (assertEqualToResult (...) ()) can also work.

metta
(Parent Bob Ann)
! (assertEqual
    (match &self (Parent Tom $x) $x)
    (empty)) ; ()
! (assertEqualToResult
    (match &self (Parent Tom $x) $x)
    ()) ; ()

Since expressions without suitable equalities remain unreduced in MeTTa, (empty) can be used to alter this behavior, when desirable, e.g.

metta
(= (eq $x $x) True)
! (eq a b) ; (eq a b)
(= (eq $x $y) (empty))
! (eq a b) ; no result

(empty) can be used to turn a total function such as if or unify into a partial function, when we have no behavior for the else-branch, and we don't want the expression to remain unreduced.

Let us note that there is some convention in how the interpreter processes empty results. If the result of match for equality query is empty, the interpreter doesn't reduce the given expression (it transforms the empty result of such queries to NotReducible), but if a grounded function returns the empty result, it is treated as partial. When a grounded function application is not reduced, e.g. (+ 1 undefined-symbol), because the function returns not the empty result, but NotReducible. This behavior may be refined in the future, but the possibility to have both types of behavior (a partial function is not reduced and evaluation continues or it returns no result stopping further evaluation) will be supported.

From nondeterministic viewpoint, (empty) removes an evaluation branch. If we consider all the results as a collection, (empty) can be used for its filtering. In the following program, (color) and (fruit) produce nondeterministic "collections" of colors and fruits correspondingly, while filter-prefer is a partially defined id function, which can be used to filter out these collections.

metta
(= (color) red)
(= (color) green)
(= (color) blue)
(= (fruit) apple)
(= (fruit) banana)
(= (fruit) mango)
(= (filter-prefer blue) blue)
(= (filter-prefer banana) banana)
(= (filter-prefer mango) mango)
(= (filter-prefer $x) (empty))
! (filter-prefer (color)) ; [blue]
! (filter-prefer (fruit)) ; [mango, banana]

In case of recursion, (empty) can prune branches, which don't satisfy some conditions as shown in this example.

Collapse

Nondeterminism is an efficient way to map and filter sets of elements as well as to perform search. However, nondeterministic branches do not "see" each other, while we may want to get the extreme element or just to count them (or, more generally, fold over them). That is, we may need to collect the results in one evaluation branch.

Reverse operation to superpose is collapse, which has the type (-> Atom Expression). It converts a nondeterministic result into a tuple.

collapse is a grounded function, which runs the interpreter on the given atom and wraps the returned results into an expression.

metta
(= (color) red)
(= (color) green)
(= (color) blue)
! (color) ; three results: [blue, red, green]
! (collapse (color)) ; one result: [(blue red green)]

Here we've got a nondeterministic result [blue, red, green] from the color function and converted it into one tuple [(blue red green)] using collapse.

The superpose function reverts the collapse result

metta
(= (color) green)
(= (color) yellow)
(= (color) red)
! (color) ; [green, yellow, red]
! (collapse (color)) ; [(green yellow red)]
! (let $x (collapse (color))
    (superpose $x))  ; [green, yellow, red]
! (superpose (1 2 3)) ; [1, 2, 3]
! (collapse (superpose (1 2 3)))
! (let $x (superpose (1 2 3)) ; [(1 2 3)]
    (collapse $x))    ; [(1), (2), (3)]

The color function gives the nondeterministic result [green, yellow, red] (the order of colors may vary). The collapse function converts it into a tuple [(green yellow red)]. And finally the superpose function in let converts a tuple back into the nondeterministic result [red, green, yellow]. The order of colors may change again due to nondeterminism.

Note that we cannot call collapse inside superpose, because collapse will not be executed before passing to superpose and will be considered as a part of the input tuple. In contrary, we cannot call superpose outside collapse, because it will cause collapse to be called separately for each nondeterministic branch produced by superpose instead of collecting these branches inside collapse.