Rules

Rules, Yacas and Mathematical Theorems

TrueAlgebra rules have a similar approach as the rules in the computer algebra system Yacas yacas.org. There are differences in the nomenclature. Yacas rules have a predicate and body which correspond to the truealgebra rule’s tpredicate and tbody methods.

The Yacas procedure mimics very well the application of mathematical theorems to mathematical expression. In Yacas, a rule has a predicate and a body. The predicate determines if the body can be applied to a Yacas mathematical object.

The Yacas rule predicate evaluate the input expression. If the predicate outputs true then the body of the rule evaluates the input expression and transforms it into the output expression of the rule. If the predicate returns false, then the input expression of the rule becomes its output expression.

When applied to mathematics, the predicate of a rule performs the role of the introduction to a mathematical theorem which specifies the scope, and specific stipulations for the theorem to be true. When a predicate returns true, it has verified that the input expression meets the conditions for the mathematical theorem. The body of the rule then transforms the input expression into an output expressions in accordance with the theorem statement.

Import and Setup

Import the necessary packages, modules, and objects.

In [1]: from truealgebra.core.rules import (
   ...:     Rule, Rules, RulesBU, JustOne, JustOneBU, donothing_rule,
   ...:     Substitute, SubstituteBU
   ...: )
   ...: from truealgebra.core.naturalrules import (
   ...:     NaturalRuleBase, NaturalRule, HalfNaturalRule
   ...: )
   ...: from truealgebra.core.expressions import (
   ...:     ExprBase, Symbol, Number, true, false,
   ...:     Container,Restricted, Assign, null, CommAssoc,
   ...: )
   ...: from truealgebra.core.settings import settings
   ...: from truealgebra.core import setsettings
   ...: from truealgebra.core.parse import Parse
   ...: from truealgebra.core.unparse import unparse
   ...: 
   ...: 
   ...: settings.parse = Parse()
   ...: settings.unparse = unparse
   ...: setsettings.set_symbol_operators("and", 75, 75)
   ...: setsettings.set_custom_bp("=", 50, 50)
   ...: setsettings.set_container_subclass("*", CommAssoc)
   ...: 
   ...: parse = settings.parse
   ...: 

RuleBase Class

RuleBase is an abstract class that provides the basis for all truealgebra rules. All RuleBase objects are called rules or truealgebra rules.

Rule, NaturalRule, and HalfNaturalRule are important RuleBase subclasses used to create rules that modify truealgebra expressions. The Rules and JustOne subclasses create rules that apply groups of other rules to truealgebra expressions.

Never say never, but it is highly unlikely at least in the near future that additional major subclasses of RuleBase will be added to TrueAlgebra. The five subclasses mentioned in the above paragraph should be sufficient for creating rules.

The RuleBase class is the basis for all truealgebra rules. All truealgebra rules have their __call__ method defined and will take arguments in the same manner as functions. All rules will take one and only one argument which has to be a truealgebra expression. The result of a rule will always be a truealgebra expression.

Abstract Methods: tpredicate and tbody

The tbody and tpredicate methods were developed using techniques from the Yacas Computer Algebra System.

All rules have tpredicate and tbody methods. The first character “t” of both names stands for “truealgebra”. A user does not need to know anything about these methods. They are hidden inside of every rule object and a user does not interact directly with them.

However it is easier to explain coherently how rules work when references are made to the tpredicate and tbody methods as in the step by step procedure below.

Primarily because of the complexity of the JustOne class, The steps used in applying TrueAlgebra rules are a more complicated than in Yacas. When a truealgebra rule takes a truealgebra expression as an argument, the steps are:

  • step 1. The rule’s __call__ method calls the tpredicate method and passes the input expression as an argument.

  • step 2a. If tpredicate returns False, then __call__ returns the input expression with no further evaluation.

  • step 2b. Otherwise, tpredicate returns a TrueThing object. The TrueThing object contains the input expression and any other pertinent information. Continue to step 3.

  • step 3. The __call__ method calls the tbody method, passing to it the TrueThing object as an argument.

  • step 4. The tbody method evaluates the input expression (and any other information in the TrueThing object) and returns (in most cases) a new algebraic expression.

  • step 5. The __call__ method returns the result of the tbody method.

The Concrete __call__ Method

The __call__ method is defined in RuleBase. As a result, rules behave like functions. Consider a python expression of the form rule(expr) where rule is a truealgebra rule and expr is a truealgebra expression. The rule here takes expr as an argument and evaluates it. The intent is that rules be viewed iand used as fancy functions.

The__call__ method. It is a fundamental part of the five step procedure outlined above. It can also implement the path and bottomup procedures.

path Attribute

The RuleBase path attribute is an empty tuple, which causes a rule to be applied to the top level of an input expression.

When the path attribute is a tuple of integers, it represents a path to a specific sub-expression inside the input expression. The rule will be applied to that specific sub-expression.

RuleBase and all of its subclasses, when instantiated, will take a path keyword argument which can be a list, tuple, or other collection. The path argument can only be positive or negative integers. The value of the path argument is assigned as a tuple to the path attribute.

bottomup Attribute

The RuleBase bottomup attribute is False. A False bottomup attribute causes a rule to be applied only to the top level of an input expression.

If the bottomup attribute is True, a rule, if applied to a Container instance will first be applied to the items in the Container instance items attribute and last be applied to the Container instance itself.

If the bottomup attribute is True, and a rule is applied to a expression with nested Container instances (Container instances that contain Container instances), the rule will be applied at all levels of the input expression starting at the lowest levels and proceeding progressively up to the top most level.

The bottomup evaluation process starts at the bottom and proceeds up to the top.

Rule Class

Rule is a subclass of RuleBase. Rule and its subclasses are the primary means of generating rules.

All Rule instanace have a predicate method, that is called by its tpredicate method. The predicate method detemrines if the rule’s tbody method will be applied to the input expression.

There is also a body meod which is called by the tbody method. The output of the body method will be the output of the tbody body and in may cases the output of the rule itself.

The Rule Instance donothing_rule

The donothing_rule rule is a Rule instance. All Rule instances have the same characteristics as donothing_rule. A donothing_rule rule always returns its input expressions, unchanged. The donothing_rule always does nothing.

The donothing_rule rule is sometimes useful as a default rule, For example it is the value for the NaturalRule predicate_rule attribute which act as a default for NaturalRule instances.

How to Create Rule Instances

To create rules other than the donothing_rule rule, a Rule subclass must be created.

IsSymEval Example

As an example create the Rule subclass IsSymEval. The Instances can be called predicate rules. They evaluate truealgebra Container expressions named issym that meet certain criteria and return true or False.

Three methods are over written below in the IsSymeval subclass.

__init__ method

The __init__ method allows for passing arguments for use by the rule. The last line of the __init__ method in the example below is very important and must always be included, otherwise the __init__ methods of parent classes will not be executed.

predicate method

The predicate method requires one positional parameter, which will be the input expression of the rule. The method must returns either True or False. If True, the body method will be invoked. If False, the input expression will be the output of the rule.

As with mathematical theorems the code in the predicate must be precise and exact, if need be using isinstance or even type functions. Mathematical theorems do not follow python duck typing conventions.

body method

The predicate method requires one positional parameter, which will be the input expression of the rule. The method must return a truealgebra expression. If this method is invoked, its output will be the output of the rule.

In [2]: class IsSymEval(Rule):
   ...:     def __init__(self, *args, **kwargs):
   ...:         self.names = args
   ...:         # The line below must be included
   ...:         super().__init__(*args, **kwargs)
   ...: 
   ...:     def predicate(self, expr):  # expr is the rule input expression
   ...:         # This method must return True or False
   ...:         return (
   ...:             isinstance(expr, Container)
   ...:             and (expr.name == 'issym')
   ...:             and (len(expr.items) > 0)
   ...:         )
   ...: 
   ...:     def body(self, expr):  # expr is the rule input expression
   ...:         if isinstance(expr[0], Symbol) and expr[0].name in self.names:
   ...:             return Symbol('true')
   ...:         else:
   ...:             return Symbol('false')
   ...:         # This method must return a truealgebra expression
   ...: 

Demonstrate an IsSymEval rule

Create the rule issym_eval_rule from IsSymEval. This rule will evaluate issym(x), issym(y), or issym(x) to true.

In [3]: issym_eval_rule = IsSymEval('x', 'y', 'z')

Case 1 - predicate not satisfied.

Create an expression with a Container instance with the name ‘wrongname’.

In [4]: expr = parse('  wrongname(x)  ')
   ...: expr
   ...: 
Out[4]: wrongname(x)

Next, apply issym_eval_rule to expr. The input expr is returned.

In [5]: issym_eval_rule(expr)
Out[5]: wrongname(x)

The name attribute of the Container instance is wrongname instead of issym as required by the predicate method. So the predicate returns False, and the rule returned the input expression without change.

Case 2 - predicate satisfied, return true

In [6]: expr = parse('  issym(y)  ')
   ...: expr
   ...: 
Out[6]: issym(y)

Next, apply issym_eval_rule to expr.

In [7]: issym_eval_rule(expr)
Out[7]: true

The result is the truealgebra expression “true”.

Case 3 - predicate satisfied, return false

In [8]: expr = parse('  issym(a)  ')
   ...: expr
   ...: 
Out[8]: issym(a)

Next, apply issym_eval_rule to expr.

In [9]: issym_eval_rule(expr)
Out[9]: false

The result is the truealgebra expression “false”.

Flatten Rule

In [10]: class Flatten(Rule):
   ....:     def predicate(self, expr):
   ....:         return isinstance(expr, CommAssoc) and expr.name == '*'
   ....: 
   ....:     def body(self, expr):
   ....:         newitems = list()
   ....:         for item in expr.items:
   ....:             if isinstance(item, CommAssoc) and item.name == '*':
   ....:                 newitems.extend(item.items)
   ....:             else:
   ....:                 newitems.append(item)
   ....:         return CommAssoc('*', newitems)
   ....: 
   ....:     bottomup = True
   ....: 
   ....: flatten = Flatten()
   ....: 

Substitute Rule

Blah blah blah.

In [11]: a = parse('a')
   ....: b = parse('b')
   ....: c = parse('c')
   ....: d = parse('d')
   ....: 

Create three rules that change Symbols

In [12]:  a_b_rule = Substitute(subdict={a: b})  # convert a to b
   ....:  b_c_rule = Substitute(subdict={b: c})  # convert b to c
   ....:  c_d_rule = Substitute(subdict={c: d})  # convert c to d
   ....: 

The rules:

  • a_b_rule changes a Symbol instance with name attribute "a" to a Symbol instance with name attribute "b".

  • b_c_rule changes a Symbol instance with name attribute "b" to a Symbol instance with name attribute "c".

  • c_d_rule changes a Symbol instance with name attribute "c" to a Symbol instance with name attribute "d".

Logic and Predicate Rules

Predicate rules are rules that evaluaate truealgebra expressions that represent logic to either true or false. The Symbol true represents mathematical truth and the Symbol false represents mathematical falsehood. Lower case names are used to prevent confusion with python True and False.

A logical expression would be expressions such as `` 3 < 7 , ``true and false, or isint(6) that are mathematically meaningful to be evaluated to true or false.. When a predicate rule cannot evaluate an input expression to either true or false, it returns the input expression.

In the example below, the predicate rule isintrule evaluates expressions of the form isint(x). The evaluation is to true if x is an integer and false otherwise. isintrule will return but not evaluate any other expressions.

Predicate Rule isintrule

The isintrule below will make a predicate evaluation of the isint predicate expression. This determines if the contents of isint is an integer number.

In [13]: class IsInt(Rule):
   ....:     def predicate(self, expr):
   ....:         return (
   ....:             isinstance(expr, Container)
   ....:             and expr.name == 'isint'
   ....:             and len(expr.items) >= 1
   ....:         )
   ....: 
   ....:     def body(self, expr):
   ....:         if isinstance(expr[0], Number) and isinstance(expr[0].value, int):
   ....:            return true
   ....:         else:
   ....:            return false
   ....: 
   ....: isintrule = IsInt()
   ....: 
   ....: # Apply isintrule, in three cases.
   ....: print(
   ....:     'case 1, isintrule( isint(4) )=  ',
   ....:     isintrule(parse('  isint(4)  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 2, isintrule( isint(sin(x)) )=  ',
   ....:     isintrule(parse('  isint(sin(x))  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 3, isintrule( cosh(4) )=  ',
   ....:     isintrule(parse('  cosh(4)  '))
   ....: )
   ....: 
case 1, isintrule( isint(4) )=   true
case 2, isintrule( isint(sin(x)) )=   false
case 3, isintrule( cosh(4) )=   cosh(4)

In case 1 above the predicate rule isintrule evaluates the isint predicate and returns true. In case 2, the rule returns false. In case 3, the rule makes no evaluation and returns its input expression.

Predicate Rule lessthanrule

The lessthanrule below will make a predicate evaluation of the < predicate expression. This determines if the first argument of < is larger than its second argument. Both arguments must be numbers.

In [14]: class LessThan(Rule):
   ....:     def predicate(self, expr):
   ....:         return (
   ....:             isinstance(expr, Container)
   ....:             and expr.name == '<'
   ....:             and len(expr.items) >= 2
   ....:             and isinstance(expr[0], Number)
   ....:             and isinstance(expr[1], Number)
   ....:         )
   ....: 
   ....:     def body(self, expr):
   ....:         if expr[0].value < expr[1].value:
   ....:             return true
   ....:         else:
   ....:             return false
   ....: 
   ....: lessthanrule = LessThan()
   ....: 
   ....: # Apply lessthanrule, in three cases.
   ....: print(
   ....:     'case 1, lessthanrule( 3.4 < 9 )=  ',
   ....:     lessthanrule(parse('  3.4 < 9  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 2, lessthanrule( 7 < 7 )=  ',
   ....:     lessthanrule(parse('  7 < 7  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 3, lessthanrule(x**2)=  ',
   ....:     lessthanrule(parse('  x**2  '))
   ....: )
   ....: 
case 1, lessthanrule( 3.4 < 9 )=   true
case 2, lessthanrule( 7 < 7 )=   false
case 3, lessthanrule(x**2)=   x ** 2

Predicate Rule andrule

Next look at the andrule which evaluates expressions such as true and false.

In [15]: class And(Rule):
   ....:     def predicate(self, expr):
   ....:         return (
   ....:             isinstance(expr, Container)
   ....:             and expr.name == 'and'
   ....:             and len(expr.items) >= 2
   ....:         )
   ....: 
   ....:     def body(self, expr):
   ....:         if expr[0] == false:
   ....:             return false
   ....:         elif expr[1] == false:
   ....:             return false
   ....:         elif expr[0] == true and expr[1] == true:
   ....:             return true
   ....:         else:
   ....:             return expr
   ....: 
   ....: andrule = And()
   ....: 
   ....: # Apply andrule, in three cases.
   ....: print(
   ....:     'case 1, andrule( true and true )=  ',
   ....:     andrule(parse('  true and true  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 2, lessthanrule( 7 < 7 )=  ',
   ....:     lessthanrule(parse('  7 < 7  '))
   ....: )
   ....: 
   ....: print(
   ....:     'case 3, lessthanrule(x**2)=  ',
   ....:     lessthanrule(parse('  x**2  '))
   ....: )
   ....: 
case 1, andrule( true and true )=   true
case 2, lessthanrule( 7 < 7 )=   false
case 3, lessthanrule(x**2)=   x ** 2

Combine Multiple Prredicate Rules

Create the predicate rule predrule by combining isintrule, lessthanrule, and andrule.

In [16]: predrule = JustOneBU(isintrule, lessthanrule, andrule)

The predrule will be used in the examples below.

NaturalRule Class

The name ‘NaturualRule’ for this class is used because the natural mathematical-like syntax of the pattern, vardict and outcome arguments used to instantiate its rules.

Natural Rule Example

In order to illustrate more features of a NaturalRule rule, the following example is a bit contrived.

In [17]: natrule = NaturalRule(
   ....:     predicate_rule = predrule,
   ....:     vardict = (
   ....:         'forall(e0, e1, suchthat(n0,  isint(n0) and (n0 < 7)))'
   ....:         #'forall(e0, e1);'
   ....:         #'suchthat(forall(n0), isint(n0) and (n0 < 7))'
   ....:     ),
   ....:     pattern='  (e0 = e1) + (x = n0)  ',
   ....:     outcome='  (e0 + x) = (e1 + n0)  ',
   ....:     outcome_rule = Substitute(
   ....:         subdict={Symbol('theta'): Symbol('phi')},
   ....:         bottomup=True
   ....:     )
   ....: )
   ....: 

Apply the rule natrule , below, to the expression``ex1``. The result, out1, of the rule is algebraically equal to ex1.

In [18]: ex1 = parse('  (cos(theta) = exp(7)) + (x = 6)  ')
   ....: print('ex1 =  ', ex1)
   ....: out1 = natrule(ex1)
   ....: print('natrule(ex1) =  ', out1)
   ....: 
ex1 =   (cos(theta) = exp(7)) + (x = 6)
natrule(ex1) =   cos(phi) + x = exp(7) + 6

How a NaturalRule Rule Works

Variable Dictionary

Initially a user enters a string as a vardict class attribute or an instantiation vardict argument. The string gets converted to a variable dictionary by a somewhat involved process. The variable dictionary is created when a NaturalRule instance is instantiated, and it remains unchanged afterwards.

A variable dictionary is a python dictionary. The dictionary keys must be truealgebra Symbol instances which will be called variables. The dictionary values must be truealgebra expressions. The values represent logic that must be satisfied in order for the variable to be matched during the matching process.

Conversion

The first step in the conversion is to parse the string The function meta_parse parses the string with line breaks and ‘;’ charaters into a sequence of python expressions. Each parsed expression if it has proper syntax will add to the variable dictionary.

In the second step an empty dictionary named vardict is defined. Each parsed expression is looked at for truealgebra Container objects named forall and suchthat. The content of the forall and suchthat objects are inspected and if the syntax is correct are made into the variable dictionary.

forall Function

The class method create_vardict does the conversion process. This method is usful to a user for debugging and investigations.

Below the vardict_string_1 gets parsed into a forall Container object that represents a mathematical function. The forall contains two symbols e0 and e1. These two symbols become keys in the vardict_dict_1 dictionary with values of true.

In [19]: vardict_string_1 = ' forall(e0, e1) '
   ....: vardict_1 = NaturalRule.create_vardict(vardict_string_1)
   ....: vardict_1
   ....: 
Out[19]: mappingproxy({e0: true, e1: true})
suchthat Function

The suchthat function below is the top level of the expression and contains two arguments. The first argument is a forall function with one argument that is a symbol.

In [20]: vardict_2 = NaturalRule.create_vardict(
   ....: #   '  suchthat( forall(n0),  isint(n0) and (n0 < 7) )  '
   ....:     '  forall( suchthat(n0, isint(n0) and (n0 < 7) ))  '
   ....: )
   ....: aa = vardict_2[Symbol('n0')]
   ....: print(aa)
   ....: 
   ....: bb = SubstituteBU(subdict={Symbol('n0'): Number(3)})
   ....: print(bb)
   ....: predrule(bb(aa))
   ....: 
isint(n0) and n0 < 7
<truealgebra.core.rules.SubstituteBU object at 0xffff7e93ce50>
Out[20]: true

The vardict_dict_2 dictionary has one key the symbol n0. The value for that key is the logical expression for n0. The logical expression contains the key, which is typical.

Pattern Matching

The tpredicate method implements the pattern method process desribed in this section.

The input expression is compared to the rule’s pattern attribute to determine if the input expression matches the pattern expression.

For a pattern to match the input expressions, both expressions and all of the subexpressions must essentially be the same or equal to each other.

Matching Without Variables

The following rules apply when the pattern or pattern or pattern subexpression is not a variable.

  • For Container expressions to match, they must be of the same python type, have the same name attribute, and have the same number of items in thier items attribute.

  • Also each item in the items attribute of the inut expression must match the corresponding item in the patern’s items attribute.

  • Number instances match Number instances. Thier value attributes must muust be equal.

  • Symbol instances will match Symbol instances if they have the same name attribute.

Matching With Variables

For an example look at the vardict and pattern attributes of natrule.

In [21]: print('vardict =   ', natrule.vardict)
   ....: #print('vardict[e0]=  ', natrule.vardict[Symbol('e0')])
   ....: #print('vardict[e1]=  ', natrule.vardict[Symbol('e1')])
   ....: #print('vardict[n0]=  ', natrule.vardict[Symbol('n0')])
   ....: 
vardict =    {e0: true, e1: true, n0: and(isint(n0), <(n0, 7))}

The variables e0 and e1 in the variable dictionary vardict, each have a value of true. which makes these variables essentially wild, to use a card playing term. These variables can match any truealgebra expression during pattern matching.

The variable n0 in the dictionary has a value of isint(n0) and n0 < 7. This value is the logical requirement that any expression must satisfy in order to match n0. The variable n0 can only match expressions that are and intreger and less than 7.

The code below shows if the number 5 can match the variable n0.

In [22]: input_5 = parse(' 5 ')
   ....: n0 = parse('  n0  ')
   ....: logic = natrule.vardict[n0]
   ....: subrule = Substitute(subdict={n0: input_5}, bottomup=True)
   ....: subed_logic = subrule(logic)
   ....: evaluation = natrule.predicate_rule(subed_logic)
   ....: print(logic)
   ....: print(subed_logic)
   ....: print(evaluation)
   ....: 
isint(n0) and n0 < 7
isint(5) and 5 < 7
true

It is a two step process, First 5 is substituted for n0 into the logic. Then the result is evaluated by the predicat_rule. The second result is``true`` which means that 5 matches n0.

Next investigate if the real 5.0 matches n0

In [23]: input_5_real = parse(' 5.0 ')
   ....: subrule = Substitute(subdict={n0: input_5_real}, bottomup=True)
   ....: subed_logic = subrule(logic)
   ....: evaluation = natrule.predicate_rule(subed_logic)
   ....: print('logic =       ', logic)
   ....: print('subed_logic = ', subed_logic)
   ....: print('evaluation =  ', evaluation)
   ....: 
logic =        isint(n0) and n0 < 7
subed_logic =  isint(5.0) and 5.0 < 7
evaluation =   false

The real 5.0 does not match n0 because it is not an integer.

match Method

The matching process is initiated by rules, but the heavy lifting is done the match methods of expressions. Normally TrueAlgebra user does not directly involk expression match methods. A user does not need to even know of the existance of the match methods.

However match methods can be used for debugging, and experience with match methods can help explain some of the magic behind natural rules.

In [24]: 
   ....: subdict = dict()
   ....: matchout = natrule.pattern.match(
   ....:     natrule.vardict,
   ....:     subdict,
   ....:     predrule,
   ....:     ex1
   ....: )
   ....: print('matchout=  ', matchout)
   ....: print('subdict=   ', subdict)
   ....: 
matchout=   True
subdict=    {e0: cos(theta), e1: exp(7), n0: 6}

The matchout is True, which causes the rule natrule to call the rule’s tbody method. The subdict dictiionary is passed to the tbobody method as well.

If matchout had been False, the rule would returned the input expression ex1 unchanged.

Substitution

When the rule’s tbody method is called, A substitution is initially performed. Look at subdict from above. subdict stands for substitution dictionary:

In [25]: print('subdict=   ', subdict)
subdict=    {e0: cos(theta), e1: exp(7), n0: 6}

Replaces any variables in the outcome expression with the appropriate expressions from the pattern matching process. The apply the outcome_rule to the outcome expression.

NaturalRule instance trys to match its pattern attribute to the input expression.

The tpredicate returns a truthy result when the input expression (ex1) matches the pattern subject to the conditions of the variable dictionary (vardict). Consider the following:

If the pattern matches the input expression, the tbody method is involked

NauralRule Subclasses

When a group of natural rules must be create that will share common attributes, it is expediant to create a NaturalRule subclass that has the common attributes and then instantiate the rules from the subclass.

NaturalRule Class Attributes

These are the NaturalRule class attribute:

predicate_rule attribute

donothing_rule

vardict attribute

empty dictionary

pattern attribute

null expression

outcome attribute

null expression

outcome_rule attribute

donothing_rule

Create a NaturalRule Subclass

The quasi python code below illustrates how to create a NaturalRule subclass. A subclass is useful when a group of rules must be created that share common attributes. All of the attribute assignments below, are optional.

 1class NaturalRuleSubclass(NaturalRule):
 2    predicate_rule = <a predicate rule>
 3    vardict = <string >
 4              # after the first instance is instantiated, this class
 5              # attribute is converted to a variable dictionary
 6    pattern = <string>
 7              # after the first instance is instantiated, this class
 8              # attribute is parsed into a truealgebra expression
 9    outcome = <string>
10              # after the first instance is instantiated, this class
11              # attribute is parsed into a truealgebra expression
12    outcome_rule = <a truealgebra rule>

HalfNaturalRule Class

The HalfNaturalRule is similar to the NaturalRule. Below shows the creatiion of the PlusIntEval subclass and its instance, the rule plus_int_eval. This rule preforms a numeric evaluation, when two integers are added together.

In a HalfNaturalRule rule there are no outcome or outcome_rule attributes. But there is a body method defined which has (besides self) two positional parameters, expr and var. The expr parameter will be the rule input expression.

The var parameter will is a var object. It will have a parmeter for every variable in the substitution dictionry subdict in [[match method]]. The attribute name will be the variable name. Each var attribute points to the value of the variable in the sustitution dictionary.

In the body method below:

var.n0

is the expression that matches the variable n0.

var.n1

is the expression that matches the variable n1.

In [26]: class PlusIntEval(HalfNaturalRule):
   ....:     predicate_rule = predrule
   ....:     vardict = (
   ....:         'Forall(suchthat(n0, isint(n0)), suchthat(n1, isint(n1)))'
   ....:     )
   ....:     pattern = '  n0 + n1  '
   ....: 
   ....:     def body(self, expr, var):
   ....:         n0 = var.n0.value
   ....:         n1 = var.n1.value
   ....:         return Number(n0 + n1)
   ....: 
   ....: plus_int_eval = PlusIntEval()
   ....: 

In a HalfNaturalRule, the body method is called by the tbody method. When a rule is applied to an input expression and finds a match, the body method result will be the result of the rule.

Rules and RulesBU

Rules is a subclass of RuleBase. Rules instances contain and apply a list of other rules. They provide to users a powerful means of organizing and grouping rules to perform mathematical operations.

Below, rules_rule is instantiated as a Rules instance. The three positional argumes are rules defined in the Subtitute rule class section above.

In [27]: rules_rule = Rules(a_b_rule, b_c_rule, c_d_rule)

The Rules class takes none to unlimited postional arguments that must be rules. These rules are assignied to the rule_list attribute in the same order they appear as arguments.

In the example below rules_rule is applied to a input expression of the symbol a.

In [28]: test_expr = Symbol('a')
   ....: rules_rule(test_expr)
   ....: 
Out[28]: d

What happened in the above example, is rule a_b_rule in the rule_list replaced the symbol a with the symbol b. The rule b_c_rule then replaced the symbol b with the symbol c. Then rule c_d_rule replaced the symbol c with symbol d which was the final output of rules_rule.

In general, When a Rules instance is applied to an input expression, the rules in its rule_list attribute will be applied in sequence from left to right. The process is the first rule in rule_list is applied to the input expression. Its output becomes the input for the next rule in rule_list. The process continues until the output of last rule in rule_list becomes the output of the Rules instance.

RulesBU

RulesBU is a subclass of Rules with the bottomup Attribute set to True.

RulesBU is useful for applying one or more rules bottom up. For a demonstration of RulesBU, create below the expression another_test_expr.

In [29]: sym_a = Symbol('a')
   ....: another_test_expr = Container('f', (sym_a, sym_a, sym_a))
   ....: another_test_expr
   ....: 
Out[29]: f(a, a, a)

Create a rule using RuleBU that contains the sames three rules as the previous example with Rules. Apply the new RuleBU rule to another_test_expr.

In [30]: rule = RulesBU(a_b_rule, b_c_rule, c_d_rule)
   ....: rule(another_test_expr)
   ....: 
Out[30]: f(d, d, d)

The three rules inside rule changed the all of the Symbol expressions names from a to b to c to d.

Bottomup Rules Inside RulesBU

Consider the case when a RulesBU instance contains a rule that has its bottomup attribute set to True. When the RulesBU instance is applied to an expression, the internal rule can be applied numerous times to the same sub-expressions inside the expression. This can lead to a great increase in the execution time for a script. This behavior is in most cases, probably not useful.

JustOne and JustOneBU

JustOne is a RuleBase subclass that is similar but different to Rules and RulesBU. Whereas a Rules rule will apply to all of the rules in its rule_list attribute to an input expression, a JustOne rule applies just one of the rules in its rules_list attribute.

In the example below, the JustOne instance justone_rule is created. The three positional arguments are rules that are assigned to the attribute justone_rule.rule_list.

In [31]: justone_rule = JustOne( a_b_rule, b_c_rule, c_d_rule)

The three rules in justone_rule.rule_list are defined in the Subtitute rule class section above.

Apply justone_rule to the symbol b. The result is the symbol c.

In [32]: test_expr = Symbol("b")
   ....: justone_rule(test_expr)
   ....: 
Out[32]: c

When a JustOne rule is applied to an input expression, the rules in the rule_list attribute are tested one by one by applying a rule’s tpredicate method to the input expression. If the tpredicate’s result is is a python False, then the next rule in rule_list is tested. But if the rule’s tpredicate result is truthy, then the rule’s tbody method is applied to the input expression and that result becomes the result of the JustOne Rule. All remaining rules in the rule_list are ignored.

In the example above, justone_rule transformed the symbol b to the symbol c. The rule in justone_rule.rule_list that accomplished this transformation was the second rule b_c_rule.

It is important to notice above, that the third rule c_d_rule was ignored. If the third rule had been applied, the symbol c would have been transformed to the symbol d.

Nesting JustOne Rules

JustOne rules can be nested. Below, justone_rule is nested inside of new_rule.rule_list

In [33]: new_rule = JustOne(a_b_rule, justone_rule, c_d_rule)
   ....: new_rule(test_expr)
   ....: 
Out[33]: c

The b_c_rule inside the nested JustOne_rule was selected to transform the b into a c.

Ignore path and bottomup

A JustOne rule will ignore the path and bottomup attributes of all rules in its rule_list.

JustOneBU

JustOneBU is a subclass of JustOne with the bottomup Attribute set to True.

Use of Path and Bottomup Attributes

Create a new RuleBase subclass to help demonstrate use of the path and bottomup attributes of rules.

In [34]: class ContainerNameX(Rule):
   ....:     def predicate(self, expr):
   ....:         return isinstance(expr, Container)
   ....: 
   ....:     def body(self, expr):
   ....:         return Container('X', expr.items)
   ....: 

Below is the definition of the test_expr expression that will be used to help illustrate the path and bottomup features. The top level of the expression is a Container instance with name f0. Nested inside at increasingly lower levels are Container instances named f1, f2, and f3. The lowest level are the Symbol instances a.

In [35]: test_expr = parse(
   ....:      'f0('
   ....:    +    'f1('
   ....:    +         'f2(),'
   ....:    +         'f2(f3(a) , f3(a))'
   ....:    +     ')'
   ....:    +  ')'
   ....: )
   ....: print('test_expr =  ' + str(test_expr))
   ....: 
test_expr =  f0(f1(f2(), f2(f3(a), f3(a))))

Use of Path Attribute

The path attribute gives a user the ability to apply a rule to a specific sub-expression inside of an expression with surgical precision.

Examples

Empty Path

As an example, create a ContainerNameX rule with an empty path. This rule has the same capabilities as a rule created with no path argument. Apply this rule to test_expr. Only the top level f0 container is changed to X.

In [36]: rule = ContainerNameX(path=())
   ....: rule(test_expr)
   ....: 
Out[36]: X(f1(f2(), f2(f3(a), f3(a))))

Index Path

A (0,) path causes the rule to be applied to the 0 index of the f0 Container instance’s items atrribute. The f1 name changes to X.

In [37]: rule = ContainerNameX(path=(0,))
   ....: rule(test_expr)
   ....: 
Out[37]: f0(X(f2(), f2(f3(a), f3(a))))

Double Index Path

A (0, 1) path causes the rule to be applied to the 1 index of the f1 Container instance’s items attribute. The second Container instance named f2 is replaced with the name X.

In [38]: rule = ContainerNameX(path=(0, 1))
   ....: rule(test_expr)
   ....: 
Out[38]: f0(f1(f2(), X(f3(a), f3(a))))

Negative Index Path

Negative indexes can be used in paths in the same way as negative index in python lists. A (0, -1) path produces the same result as the last example

In [39]: rule = ContainerNameX(path=(0, -1))
   ....: rule(test_expr)
   ....: 
Out[39]: f0(f1(f2(), X(f3(a), f3(a))))

Index Path Length

A path can be of any length needed. Here, the second Container instance named``f3`` is renamed as X.

In [40]: rule = ContainerNameX(path=(0, 1, 1))
   ....: rule(test_expr)
   ....: 
Out[40]: f0(f1(f2(), f2(f3(a), X(a))))

Path Errors

An error is created when a path is improper. The default in TrueAlgebra is to capture these errors and print out an error message. Also the sub-expression where the error occurred will become a Null instance.

Type Error in Path

Below is the error message when an index of a path is of a type other than int.

In [41]: rule = ContainerNameX(path=(0, 'one'))
   ....: rule(test_expr)
   ....: 
TRUEALGEBRA ERROR!
type error in path
Out[41]: f0( <NULL> )

Index Error in Path

Below is the error message when an index in the path is too large for the corresponding Container instance items attribute.

In [42]: rule = ContainerNameX(path=(0, 1, 100))
   ....: rule(test_expr)
   ....: 
TRUEALGEBRA ERROR!
index error in path
Out[42]: f0(f1(f2(),  <NULL> ))

Path too Long

Atoms, such as Number and Symbol instances do not contain sub-expressions. When a path leads to an atom and still has superfluous indexes, this error message occurs:

In [43]: rule = ContainerNameX(path=(0, 1, 1, 0, 3))
   ....: rule(test_expr)
   ....: 
TRUEALGEBRA ERROR!
Path too long, cannot enter atom expressions.
Out[43]: f0(f1(f2(), f2(f3(a), f3( <NULL> ))))

Use of Bottomup Attribute

A rule applied bottom up to an expression will be applied to the expression and all available sub-expressions within the expression. The application of the rule starts at the bottom, lowest achievable level of the expression and progresses up until the rule is applied to the top level of the expression.

Apply a ContainerNameX rule bottomup.

In [44]: rule = ContainerNameX(bottomup=True)
   ....: rule(test_expr)
   ....: 
Out[44]: X(X(X(), X(X(a), X(a))))

Every Container instance name throughout test_expr was changed to X.

Path and Bottomup

A rule is applied first to its path if it is non-empty, and second the rule is applied bottom up if its bottomup attribute is True.

In [45]: rule = ContainerNameX(path=(0, 1), bottomup=True)
   ....: rule(test_expr)
   ....: 
Out[45]: f0(f1(f2(), X(X(a), X(a))))

In the above example the Container names at path (0,1) and its sub-expressions are changed to X.

Restricted Class Expressions

The class Restricted is a subclass of Container. Both classes have the same name and items atrributes. But some of their methods differ and as a result the Restricted class instances respond differently to rules applied with to a path or bottom up.

A rule with a nonempty path can be successfully applied when pointed to a Restricted instance, but will generate an error when pointed to the sub-expressions in the instance’s items attribute. The internal sub-expressions are restricted to rules applied using path.

In [46]: another_test_expr = Restricted('restricted', (
   ....:     Container('f', ()),
   ....:     Container('g', ()),
   ....:     Container('h', ()),
   ....:     ))
   ....: another_test_expr
   ....: 
Out[46]: restricted(f(), g(), h())

Now apply the rule with a path to all three sub-expressions inside the Restricted expression. In all cases an error is generated.

In [47]: rule = ContainerNameX(path=(0,))
   ....: rule(another_test_expr)
   ....: 
TRUEALGEBRA ERROR!
path cannot enter Restricted instnce
Out[47]:  <NULL> 
In [48]: rule = ContainerNameX(path=(1,))
   ....: rule(another_test_expr)
   ....: 
TRUEALGEBRA ERROR!
path cannot enter Restricted instnce
Out[48]:  <NULL> 
In [49]: rule = ContainerNameX(path=(2,))
   ....: rule(another_test_expr)
   ....: 
TRUEALGEBRA ERROR!
path cannot enter Restricted instnce
Out[49]:  <NULL> 

Apply a ContainerNameX rule bottom up. The Restricted expression’s name is changes but not he names of the sub-expressions insde the Restricted expression are not chaged.

In [50]: rule = ContainerNameX(bottomup=True)
   ....: rule(another_test_expr)
   ....: 
Out[50]: X(f(), g(), h())

Assign Class Expressions

The class Assign is a subclass of Container. The first item (with index 0) in the items attribute of an Assign instance is protected from the application of a rule through path or bottomup actions.

For a demonstration, create yet another test expression that has an Assign instance containing sub-expressions f(), g(), and h().

In [51]: yet_another_test_expr = Assign('assign', (
   ....:     Container('f', ()),
   ....:     Container('g', ()),
   ....:     Container('h', ()),
   ....:     ))
   ....: yet_another_test_expr
   ....: 
Out[51]: assign(f(), g(), h())

Path Demonstatration

Now apply the ContainerNameX rule with a path pointing to f(), the first item in assign(f(), g(), h()):

In [52]: rule = ContainerNameX(path=(0,))
   ....: rule(yet_another_test_expr)
   ....: 
TRUEALGEBRA ERROR!
Assign 0 item closed to path
Out[52]:  <NULL> 

The output is null accompanied by an error message. The rule cannot be applied to the first item f().

Now apply the rule, successfully, using a path attribute to g() and `h().

In [53]: rule = ContainerNameX(path=(1,))
   ....: rule(yet_another_test_expr)
   ....: 
Out[53]: assign(f(), X(), h())
In [54]: rule = ContainerNameX(path=(2,))
   ....: rule(yet_another_test_expr)
   ....: 
Out[54]: assign(f(), g(), X())

The path can be succussfully directed to any item except the first in the items attribute of an Assign instance.

Bottomup Demonstration

Apply a ContainerNameX rule bottom up. The Assign expression’s name is changed. All of the names of the sub-expressions inside the Assign expression are changed except for the first. The first sub-expression is protected.

In [55]: rule = ContainerNameX(bottomup=True)
   ....: rule(yet_another_test_expr)
   ....: 
Out[55]: X(f(), X(), X())

Tips

Of course, if a rule already exists, its path and bottomup attributes can be reassigned. However, if a existing rule is to be applied bottomup or pathfor a one time use the quasi code below shows the recommended procedures. code below .

temprule = RulesBU(rule)

temprule acts the same as rule except the bottomup attriute is True.

newrule = Rules(rule, path=<a path>)