Skip to content

Control flow

MeTTa has several specific constructs that allow a program to execute different parts of code based either on pattern matching or logical conditions.

if

if was already covered in this tutorial. But let us recap it as a part of stdlib.

The if statement implementation in MeTTa can be the following function

metta
(: if (-> Bool Atom Atom $t))
(= (if True $then $else) $then)
(= (if False $then $else) $else)

Here, the first argument (condition) is Bool, which is evaluated before executing the equality-query for if.

The next two arguments are not evaluated and returned for the further evaluation depending on whether the first argument is matched with True or False.

The basic use of if in MeTTa is similar to that in other languages:

metta
(= (foo $x)
   (if (>= $x 0)
       (+ $x 10)
       (* $x -1)
   )
)
! (foo 1)  ; 11
! (foo -9) ; 9

Here we have a function foo that adds 10 to the input value if it's grater or equal 0, and multiplies the input value by -1 otherwise. The expression (>= $x 0) is the first argument of the if function, and it is evaluated to a Bool value. According to that value the expression (+ $x 10) or (* $x -1) is returned for the final evaluation, and we get the result.

In contrast to other languages, one can pass a variable to if and it will be matched against equalities with both True and False. Consider the following example

metta
! (if $x (+ 6 1) (- 7 2))
(= (foo $b $x)
   (if $b
       (+ $x 10)
       (* $x -1)
   )
)
! ((foo $b 1) $b) ; [(-1 False), (11 True)]

foo accepts the condition for if, and when we pass a variable, both branches are evaluated with the corresponding binding for $b.

if can also remain unreduced:

metta
! (if (> $x 0) (+ $x 5) (- $x 5))

In this expression, (> $x 0) remains unreduced. Its overall type is Bool, but it can't be directly matched against neither True nor False. Thus, no equality is applied.

let

let has been briefly described in another tutorial. Here, we will recap it.

The let function is utilized to establish temporary variable bindings within an expression. It allows introducing variables, assign values to them, and then use these values within the scope of the let block.

Once the let block has run, these variables cease to exist and any previous bindings are re-established. Depending on the interpreter version, let can be either a basic grounded function, or be implemented using other primitives. Let us consider its type

metta
! (get-type let)

The first argument of let is a pattern of Atom type, which is not evaluated. The second argument is the value, which is reduced before being passed to let The third parameter is an Atom again. An attempt to unify the first two arguments is performed. If it succeeds, the found bindings are substituted to the third argument, which is then evaluated. Otherwise, the empty result is returned.

Consider the following example:

metta
(= (test 1) 1)
(= (test 1) 0)
(= (test 2) 2)

! (let $W (test $X) (println! ("test" $X => $W)))

The code above will print:

metta
("test" 1 => 1)
("test" 1 => 0)
("test" 2 => 2)

and return three unit results produced by println!. It can be seen that variables from both the first and the second arguments can appear in the third argument.

The following example shows the difference between the first two arguments.

metta
(= (test 1) 2)
! (let 2 (test 1) YES) ; YES
! (let (test 1) 2 NO ) ; empty

In case of (let 2 (test 1) YES), (test 1) is evaluated to 2, and it can be unified with the first argument, which is also 2. In case of (let (test 1) 2 NO), (test 1) is not reduced, and it cannot be unified (as a pattern) with 2, so the overall result is empty.

This example also shows that variables are not mandatory in let. What is needed is the possibility to unify the arguments. This allows using let for chaining operations, and this chaining can be conditional if the first operation returns some value, e.g.

metta
(= (is-frog Sam) True)
(= (print-if-frog $x)
   (let True (is-frog $x)
     (println! ($x is frog!))))
! (print-if-frog Sam) ; ()
! (print-if-frog Ben) ; empty

Another basic use of let is to calculate values for passing them to functions accepting arguments of Atom type, for example:

metta
(Sam is 34 years old)
! (match &self ($who is (+ 20 14) years old) $who) ; empty
! (let $r (+ 20 14)
   (match &self ($who is $r years old) $who)) ; Sam

Since the first argument can be not only a variable or a concrete value, but also an expression, let can be used for deconstructing expressions

metta
(= (fact Sam) (age 34))
(= (fact Sam) (color green))
(= (fact Tom) (age 14))
! (let (age $r) (fact $who)
   ($who is $r)) ; [(Tom is 14), (Sam is 34)]

The branches not corresponding to the (age $r) pattern are filtered out.

let*

When several consecutive substitutions are required, let* can be used for convenience. The first argument of let* is Expression, which elements are the required substitutions, while the second argument is the resulting expression. In the following example, several values are subsequently calculated, and let* allows making it more readable (notice also how pattern matching helps to calculate minimum and maximum values together with their absolute difference in one if).

metta
(Sam is 34)
(Tom is 14)
(= (person-by-age $age)
   (match &self ($who is $age) $who))
(= (persons-of-age $a $b)
   (let* ((($age-min $age-max $diff)
           (if (< $a $b)
               ($a $b (- $b $a))
               ($b $a (- $a $b))))
          ($younger (person-by-age $age-min))
          ($older   (person-by-age $age-max))
         )
         ($younger is younger than $older by $diff years))
)
! (persons-of-age 34 14)

Another case, for which let* can be convenient, is the consequent execution of side-effect functions, e.g.

metta
(Sam is 34)
(= (age++ $who)
   (let* (($age (match &self ($who is $a) $a))
          ( ()  (println! (WAS: ($who is $age))))
          ( ()  (remove-atom &self ($who is $age)))
          ( ()  (add-reduct &self ($who is (+ $age 1))))
          ($upd (match &self ($who is $a) $a))
          ( ()  (println! (NOW: ($who is $upd)))))
        $upd
   )
)
! (age++ Sam) ; 35

case

Another type of multiway control flow mechanism in MeTTa is the case function, which was briefly mentioned in the tutorial. It turns let around and subsequently tests multiple pattern-matching conditions for the given value. This value is provided by the first argument. While the formal argument type is Atom, it will be evaluated. The second argument is a tuple, which elements are pairs mapping condition patterns to results.

metta
(Sam is Frog)
(Apple is Green)
(= (test $who)
   (case (match &self ($who is $x) $x)
    (
        (42   "The answer is 42!")
        (Frog "Do not ask me about frogs")
        ($a   ($who is $a))
    )))
! (test Sam) ; "Do not ask me about frogs"
! (test Apple) ; (Apple is Green)
! (test Car) ; empty

Cases are processed sequentially from the first to the last. In the example above, $a condition will always be matched, so it is put at the end, and the corresponding branch is triggered, when all the previous conditions are not met. Note, however, that $a is not matched against the empty result in the last case.

In order to handle such cases, one can use Empty symbol as a case pattern (in some versions of the interpreter, Empty is the dedicated symbol which (empty) is evaluated to). The following code should return "Input was really empty":

metta
! (case (empty)
    ((Empty "Input was really empty")
    ($_   "Should not be the case"))
  )

In the current version of MeTTa (v0.1.12), Empty can be matched against a variable. This means that the result of (empty) can be matched against $. In this situation, if we want to catch Empty case, we need to place it before $ case.

Let us consider the use of patterns in case on example of the rock-paper-scissors game. There are multiple ways of how to write a function, which will return the winner. The following function uses one case with five branches:

metta sandbox
metta
(= (rps-winner $x $y)
   (case ($x $y)
     (((Paper Rock) First)
      ((Scissors Paper) First)
      ((Rock Scissors) First)
      (($a $a) Draw)
      ($_ Second))
   )
)
! (rps-winner Paper Scissors) ; Second
! (rps-winner Rock  Scissors) ; First
! (rps-winner Paper Paper   ) ; Draw

One could also write a function, which checks if the first player wins, and use it twice (for ($x $y) and ($y $x)). This could be more scalable for game extensions with additional gestures, and could be more robust to unexpected inputs (although this should be better handled with types). You can try experimenting with different approaches using the sandbox above.