A tutorial on chess search
First some good news! Solid and potent MeTTa code can be built with only about a dozen fairly simple programming constructs. The possible downside though is that MeTTa, while a unique language, does look somewhat like LISP with nested, balanced parenthesis to mind. However, a bit of practice and an IDE that tracks parenthesis nesting with colors works wonders.
Below is a crash course covering a few code constructs to add and manipulate atoms, assign variables, define rewrite rules, handle conditional execution, run a sequence of statements, loop, and manage the combinatorial explosion with the superpose, match, and collapse functions.
We'll use these in the chess game in the next section.
Atomspace, where all MeTTa code and data reside, can be subdivided into sub-spaces. The default sub-space is known as &self. For this tutorial we'll just use &self although it is possible to create arbitrary spaces (eg., &fancyatoms).
After you run any example in this tutorial you can check your atomspace by clicking the button to display atomspace at the top of each tutorial page. To tidy things up as you work through the examples click the button to reset atomspace (also located at the top of each tutorial page). Note that if you run a code box repeatedly that defines a function, the same function may be defined more than once -- if that happens it runs more than once. If it runs more than once you get duplicate results. So, please click "Reset Atomspace" if you get duplicate results. Code boxes can be reset individually and those with a challenge question will have a "Cheat" button if you just feel like displaying the answer.
The exclamation point ! will kick MeTTa into action. Think of it like the ignition on a car. If you submit an expression to MeTTa without a preceding exclamation point, MeTTa simply adds the expression to atomspace. But if you precede the expression with a "!", MeTTa executes the expression. Submit your entire program without an exclamation point and it is loaded into atomspace, ready to run. When you want to run it, submit to MeTTa something like "!(MainLoop)". Assuming you have a function like "(= (MainLoop) (action))" which starts your program, your entire program runs based upon one !. If you submit "(MainLoop)" without the "!" prefix MeTTa just adds "(MainLoop)" to atomspace and not much happens. Often a single ! operator per call to MeTTa is enough. If the invoked function like "!(MainLoop)" calls other functions, the first ! is effective and active for an entire chain sequence of function calls. Examples below should clarify this.
The quickest way to add atoms to atomspace is by declaration in your program. The following example will add "(my declared atom)" to the &self sub-space. Note that if you declare atoms using this simple approach such atoms are placed in the &self sub-space only. When using this style to add atoms, simple declaration is enough -- there is no need to prefix the expression with "!".
(my declared atom)Another way of adding atoms is by using add-atom as in the following example. When you use add-atom it is necessary to specify the sub-space within atomspace (&self for the tutorial). In this example we need to prefix the statement with "!" to force MeTTa to execute the expression, to actually add "(my second declared atom)" to atomspace.
Try running the code example below both with and without the "!" prefix. By displaying and resetting atomspace after each run you can observe the difference. Again, prefixing with "!" will trigger immediate execution of the expression. Omission of the "!" prefix will add the entire expression to atomspace without execution. This is a subtle but important difference but easy to see by checking atomspace.
!(add-atom &self (my second declared atom))The basic format for removing atoms from atomspace is identical to add-atom except we use remove-atom. The following example first adds an atom. After you run the following code, check to see if "(my third atom)" is in atomspace. Then simply change add-atom to remove-atom in the code block (or click "Cheat") to delete "(my third atom)" from atomspace. Run the changed code and then click to display atomspace. The "(my third atom)" atom will be removed.
!(add-atom &self (my third atom))All variables in MeTTa are prefixed with the dollar sign. The let construct is useful for assigning values to variables and further computing. The first argument to let is the variable to be assigned a value, and the second argument its value. Lastly the third argument can make use of the variable. In the example below, the value of \$var1 is returned by the let statement.
!(let $var1 Lighthouse $var1)Expressions in the second and third arguments of let are evaluated, or "reduced," if MeTTa can find a way to further execute. In the above example the symbol "Lighthouse" was returned from the let construct since there was nothing left to evaluate or execute with just "Lighthouse" assigned to our variable.
In the example below the second argument's expression simply adds 1 and 1. The result 2 is assigned to \$var1, and then the third argument uses the variable (now with value 2) inside a small list atom. The value assigned to the variable is available to the third argument. The third argument is the result of the let construct.
!(let $var1 (+ 1 1) (result $var1))Challenge: modify the below code example so that "(result 2)" is added to atomspace.
!(let $var1 (+ 1 1) ???)A fancier variation of the let construct is let*. The first argument to let* is a list of variables and their assignments. The second argument allows you to compute with any of the variables created in the first argument. In the example below two variables are each assigned their own values in the first argument list and then added together in the second argument to let*. The second argument is the result returned by the let* construct.
It's important to note that in the let* construct you can reference variables anywhere inside the same let* after they are declared.
!(let*
; first argument is a list of variables with their assigned values
( ($var1 (+ 1 1)) ($var2 (+ 1 $var1) ) )
; second argument is the result to be returned by let*
(+ $var1 $var2)
) Challenge: modify the code block below to create another variable \$var3 with a value of 2. Return the result of \$var2 less \$var3.
!(let*
( ($var1 (+ 1 1)) ($var2 (+ 1 $var1) ) ???? )
(- $var2 ??? )
) You can use the match construct mixing variables and symbols to search for patterns in atomspace. The following example adds atoms "(Seattle is a large city)" and "(Ellensburg is a medium-sized city)" to atomspace. Then a match statement uses a variable named \$city to match the large city atom. The last argument, \$city, is returned as the result.
(Seattle is a large city)
(Ellensburg is a medium-sized city)
!(match &self ($city is a large city) $city)Challenge: Run the first code box below to add "(Leavenworth is a small city)" to atomspace. Verify that it is in atomspace. Modify the second code box below to find and then remove "(Leavenworth is a small city)" from atomspace. Check to verify it has been removed.
(Leavenworth is a small city)!(match &self ???? ????)Rewrite rules, also known as functions and equalities, also use variables, and have the distinct form of:
(= (pattern) (result))
The following example matches two variations of a light switch pattern. If called with the pattern of switch on, it returns the result off, and vice versa. Note that when the function is submitted to MeTTa, the function is added to atomspace, so if resubmitted, another copy of the function is added. If you run the code box twice the function will be in atomspace twice and be executed twice. You can use remove-atom to remove the function from the &self space.
(= (switch $state) (if (== $state on) off on))
!(switch on)Challenge: Create a function that rewrites the input pattern "(onions carrots halibut)" with the output result "(light meal)". Then invoke the function and return the result.
(= ??? ???)
!(onions carrots halibut)With if statements MeTTa uses the functional style for conditional tests. Instead of coding "if \$a > 5..." as in a language like Python, place the test operator first, eg "(if (> \$a 5)...)" in MeTTa. Multiple conditions result in expressions looking like the following example. The true execution path immediately follows the conditional test expression and the false second.
(= (WeekendTest $day $time)
(if (and (== $day Sunday) (< $time 1000))
(stay in bed)
(get up)))
!(WeekendTest Sunday 0900)Challenge: modify the test so that if it is cloudy, one stays in bed regardless of day or time.
(= (WeekendTest2 $day $time $conditions)
(if ??? (and (== $day Sunday) (< $time 1000) ???)
(stay in bed)
(get up)))
!(WeekendTest2 Sunday 0900 cloudy)The case statement is fairly standard. The example below selects cases depending upon what state a game is in. If there is no match the "otherwise" condition is \$_ and will run "(Invalid)"
(= (Run_state $state)
(case $state
(
(start (StartGame)) ; start game
(move (MakeMove)) ; AI move
(reset (Reset)) ; Reset game
(quit (Quit)) ; Exit game
($_ (Invalid)) ; OTHERWISE
)))
!(Run_state move)So far we have looked at basically ordinary programming techniques, but the real power in MeTTa is its ability to sift and search through a massive number of possible combinations of code and data given some input. By default MeTTa insists on executing every possible path through your code. It returns all possible results in no particular order. It might sound like a mess but it's actually easy to manage this situation with some simple techniques that enable you, in general, to control a combinatorial explosion.
A typical strategy with MeTTa is to use superpose to generate lots of possibilities, filter the results using match and conditional checks, and then collapse to halt the process and return the results in parenthesis. If you neglect to end the process with a collapse call, MeTTa might back up later in your code unexpectedly.
Suppose you have a function called "DevisePlan" which inputs a single argument, \$strategy. The possible values for \$strategy are "Speed," "LowCost," and "Efficiency." Each of these strategies has a unique function, here made simple. Using superpose allows you overload the "DevisePlan" function and return all three plans in one call -- it runs three times. Notice that "DevisePlan" is called with a superpose of all possible arguments, not with a single argument.
(= (SpeedPlan) (1st Plan Complete))
(= (CostPlan) (2nd Plan Complete))
(= (EfficiencyPlan) (3rd Plan Complete))
(= (DevisePlan $strategy)
(case $strategy
(
(Speed (SpeedPlan)) ; call SpeedPlan function above
(LowCost (CostPlan)) ; call CostPlan
(Efficiency (EfficiencyPlan)) ; call EfficiencyPlan
($_ (Invalid)) ; OTHERWISE
)))
!(DevisePlan (superpose (Speed LowCost Efficiency)))Using superpose can generate lots of results. Real world problems likely require filtering, often using the match construct and other constraints, for context. MeTTa has a special expression (empty) which helps you discontinue results by particular context.
In the example below, the "EfficiencyPlan2" function finds the atom "(efficiency not available)" using match. The execution argument in match below is the special (empty). That means that nothing is returned from the first "EfficiencyPlan2" and this option is filtered out of the results.
(efficiency not available)
(= (SpeedPlan2) (1st Plan Complete))
(= (CostPlan2) (2nd Plan Complete))
(= (EfficiencyPlan2)
(match &self (efficiency not available) (empty)))
(= (EfficiencyPlan2)
(match &self (efficiency available) (3rd Plan Complete)))
(= (DevisePlan2 $strategy)
(case $strategy
(
(Speed (SpeedPlan2))
(LowCost (CostPlan2))
(Efficiency (EfficiencyPlan2))
($_ (Invalid)) ; OTHERWISE
)))
!(DevisePlan2 (superpose (Speed LowCost Efficiency)))Any time you execute (empty) within a function execution will immediately stop and nothing is returned for the particular case at hand. (empty) is a quick filtering technique. The following function filters numbers, returning those passed less than 10. If a number fails the test condition (empty) halts execution for that particular call to "NumberCheck".
(= (NumberCheck $number)
(if (< $number 10)
$number
(empty)
)
)
!(NumberCheck (superpose (10 2 15 3 5 1 0 25)))superpose will return results in a format with values separated by commas. For example, "[2,3,5,1,0]" is returned from the example above. This format with values divided by commas is not easy for us to deal with programmatically in MeTTa, so we use collapse with eg "[2,3,5,1,0]" as the argument. This formats the results in an ordinary MeTTa expression in parenthesis "(2 3 5 1 0)".
(= (NumberCheck2 $number)
(if (< $number 10)
$number
(empty)
)
)
!(collapse (NumberCheck2 (superpose (10 2 15 3 5 1 0 25))))Use of collapse also prevents MeTTa from surreptitiously backing up and trying other variations in your code. As has been emphasized, MeTTa insists on executing all possible paths through your code. However if you use collapse you create a boundary -- after evaluation MeTTa will not back up and try some other avenue through your code!
Challenge: Cloning the above code, input all days of the week and filter out weekend days, returning a list with Monday through Friday.
(= (DayCheck $day)
(if (??? (== $day Saturday) (== $day Sunday))
???
$day
)
)
!(??? (DayCheck (??? (Monday Tuesday Wednesday Thursday Friday Saturday Sunday))))Looping can often be done with old school LISP-ish recursion. This function adds a list of passed numbers. MeTTa provides car-atom, cdr-atom, and cons-atom for manipulating lists.
(= (addit_list $integer_list)
(if (== $integer_list ())
0
(+ (car-atom $integer_list) (addit_list (cdr-atom $integer_list)))))
!(addit_list (4 6 3))If at all possible, make your data structures simple one dimensional atom expressions in atomspace. Small atoms can be retrieved with random access using match. If you use long lists and search with LISP-ish recursion in multiple dimensions MeTTa will have to perform sequential searches. Sequential search is slower than random access. Also, most people don't find recursion particularly easy to read.
Atoms and match are defining features of MeTTa. MeTTa is not LISP although it shares some similarities. Both versions below work but the first is preferable in MeTTa.
;small atoms for tools
(tool hammer)
(tool screwdriver)
(tool saw)
;random access lookup for small atoms, easy and fast!
(= (find_tool_match $tool) (match &self (tool $tool) found))
!(find_tool_match hammer)
;----------------------------------
;avoid searching through lists which is confusing looking and less efficient
(= (find_tool_match_recursion $tool $list)
(if (== $list ())
(empty)
(if (== $tool (car-atom $list))
found
(find_tool_match_recursion $tool (cdr-atom $list)))))
;don't use a list of tools: you'll have to search through it sequentially
!(find_tool_match_recursion saw (hammer screwdriver saw))Eventually every programmer needs to run a sequence of statements. progn is used to execute statements for side effects. The value returned is the result of the last expression.
(= (PrognTest)
(progn
(add-atom &self (progn test)) ;add a test atom
(match &self (progn $t) $t) ;retrieve the atom and return "test"
)
)
!(PrognTest)