For each function to be used, it should first be declared with RuleBase("function",{argument list});. So, a new rules database for f(x,y) would be created by typing RuleBase("f",{x,y});
After that, rules can be added for this function. A rule simply states that, for a specific function with a specific arity, if a certain predicate is true, then evaluate some expression and return its result, which then is to be treated as its simplified form. As a simple example, consider:
In( n ) = RuleBase("f",{n}); |
In( n ) = PostFix("f"); |
There is a function Function , defined in the standard scripts, that allows you to define simple functions. A very simple example would be
Function First(list) list[[ 1 ]] ; |
Function First(list) |
Also, the := operator is overloaded to also be able to define functions that way. So the function first could also have been defined by simply typing
First(list):=list[[1]] ; |
Function("ForEach",{foreachitem,foreachlist,foreachbody}) |
Finally, HoldArg("function",argument) specifies that the argument argument should not be evaluated before being bound to that variable. This holds for foreachitem and foreachbody , since foreachitem specifies a variable to be set to that value, and foreachbody is the expression that should be evaluated after that variable is set.Inside the body of the function definition there are calls to Local(...). 'Local' declares some local variable that will only be visible within a block [ ... ]. The command MacroLocal, works almost the same. The difference is that it evaluates its arguments before performing the action on it. This is needed in this case, because the variable foreachitem is bound to the variable to be used, and it is the variable it is bound to we want to make local, not foreachitem itself. MacroSet works similarly, it does the same as Set, except that it also first evaluates the first argument, thus setting the variable requested by the user of this function. The Macro... functions in the built-in functions generally perform the same action as their non-macro versions, apart from evaluating an argument it would otherwise not evaluate.
To see the function in action, you could type:
ForEach(i,{1,2,3}) [Write(i);NewLine();]; |
Note: the variable names foreach... have been chosen so they won't get confused with normal variables you use. This is a major design flaw in this language. Suppose there was a local variable foreachitem, defined in the calling function, and used in foreachbody. These two would collide, and the interpreter would use only the last defined version. In general, when writing a function that calls Eval , it is a good idea to use variable names that can not easily be mistaken. This is generally the single largest cause of bugs when writing programs in Yacas. This issue should be addressed in the future.
You can just type things like a+b*c , which would of course be interpreted as a+(b*c) . Typing (a+b)*c would naturally perform the addition before the multiplication. Precedence can be set for the defined operators (precedence determines whether addition is performed before multiplication, for instance, it implicitly determines where the brackets are placed internally). General function calls have the form func(arg1,arg2); etc. Function calls don't need arguments: Exit(); will suffice in that case.
Atoms are generally thought of as strings. You can always separate things with spaces or newlines. The symbolic signs like +-*= etc. are treated as separate characters from the alphabetic and numerical ones, so a+2 will be separated into the tokens a, + and 2 by the lexical analyzer. The standard lexical analyzer is case-sensitive.
While (i < 10) |
Note: the [ and ] brackets should generally be surrounded by spaces. This is because [[ and ]] are valid brackets also (for accessing items in a list).
Strings are generally represented with quotes around them, like "this is a string" . \ in a string will unconditionally add the next character to the string, so a quote can be added with \" .
Lists of atoms are generally interpreted in the following way: the first atom of the list is some command, and the atoms following in the list are considered the arguments.
There is one main difference with normal Lisp, as far as evaluation is concerned. Evaluation is sometimes abused as a mechanism for simplifying an expression. In that case, if an expression can not be simplified, the unsimplified version is returned. This is behaviour specific to a computer algebra system. Also, the first element of the list, which is a command to be performed, is also considered a type specifier for the object (the list).
You can tell the interpreter that a function can see the local variables from the calling environment by using the UnFence command on the specified function.
You can specify that arguments are not evaluated before they are bound to the parameter: HoldArg("foo",a); would then declare that the a arguments in both foo(a) and foo(a,b) should not be evaluated before bound to a. This is useful for procedures performing actions based partly on a variable in the expression, like integration, differentiation, looping, etc., and will be typically used for functions that are algorithmic and procedural by nature.
Then declaring a rule for that function goes through Rule. The arguments to Rule merit some explanation: Rule("foo",arity,precedence,predicate,body); specifies that for function foo with arity (foo(a,b) has arity 2), there is a rule that if predicate is true, then body should be evaluated, and the original expression replaced by the result. precedences go from low to high, so a rule with precedence 0 will be tried before a rule with precedence 1. You can then later on specify parsing properties for the function if you like. If none of the predicates is true, the function with its arguments evaluated is returned.
This scheme is slightly slower for ordinary functions that just have one rule (with the predicate True), but it is desired behaviour for symbolic maninpulation. You can slowly build up functions using this scheme, testing the properties underway.
If the object is a compound object (internally, a list), the engine tries to find out if it is an internal command. If so, that is called. Otherwise, it possibly is a user-defined function (a "rule database"), and in that case the rule database is applied to it. If none of these are true, the object is returned unevaluated.
The main properties of this scheme are: when objects are bound to variables, they generally are evaluated first. When referencing the value of that variable, it isn't reevaluated again. Default behaviour of the engine is to return the original expression if it couldn't be evaluated. This is desired behaviour if evaluation is abused for simplifying expressions.
One major design flaw in Yacas (one other functional languages like LISP also have) is that when some expression is re-evaluated in another environment, the local variables contained in the expression to be evaluated might have a different meaning. This means for now that care has to be taken not to use too obvious variable names in functions that also call Eval.
One example of how rules can produce unwanted results is the rule a*0=0. This would always seem to be true. However, when a is a vector, like a:={b,c,d} , then a*0 should actually return {0,0,0} , that is, a null vector. The rule a*0 -> 0 actually changes the type of the expression from a vector to a integer! This can have severe consequences when other functions using this expressions as an argument expect a vector, or even worse, have a definition of how to work on vectors, and a different one for working on numbers.
This chapter intends to desribe the coding style and conventions applied in Yacas in order to make sure the engine always returns the correct result. This is an attempt at fending off such errors by combining rule-based programming with a clear coding style which should make these mistakes impossible.
It is assumed that the operators working on arguments, like Sin or *, always have the same properties regardless of the arguments. The Taylor series expansion of Sin(a) will be the same regardless of whether a is a real number, complex number or even a matrix. The same trigonometric identities should hold for Sin, regardless of the type of a, too.
If a function is defined which does not adhere to these rules when applied to another type, a different function should be defined.
By default, if a variable has not been bound yet, it is assumed to be a number. If it is in fact a more complex object, like a vector, then you can declare a to be an 'incomplete type' vector, using Object("IsVector",a). This subexpression will evaluate to a if and only if a is a vector at that moment of evaluation. Otherwise it returns unevaluated, and thus stays an incomplete type.
So this means the type of a variable is numeric unless otherwise stated by the user, using the "Object" command. No rules should ever work on incomplete types. It is just meant for delayed simplification.
The topic of implicit type of an object is important, since many rules often implicitly need to be able to assume something is a number.
The implementor of a rule set can specify the order in which rules should be tried. This can be used to let the engine try more specific rules before trying less specific rules.
The example mentioned in the introduction would be solved by a*b (b a vector) -> return vector of each component multiplied by a. a*0 -> 0 So vector multiplication would be tried first. The ordering of the precedence of the rules in the standard math scripts is currently:
The options you have for debugging a faulty function are
In( 1 ) = TraceRule(x+y)2+3*5+4; |
ENTER:2+3*5+4 |