Generally, all core functions have plain names and almost all are not 'bodied' or infix operators. The file "src/yacasapi.cc" in the source tree lists declarations of all kernel functions; consult it for reference. For many of the core functions, the script library already provides convenient aliases. For instance, the addition operator "+" is defined in the script scripts/standard while the actual addition of numbers is performed through the built-in function MathAdd.
The type of an object is returned by the built-in function Type, for example:
In> Type(a); Out> ""; In> Type(F(x)); Out> "F"; In> Type(x+y); Out> "+"; In> Type({1,2,3}); Out> "List"; |
Note that atoms that result from an Atom() call may be invalid and never evaluate to anything. For example, Atom(3X) is an atom with string representation "3X" but with no other properties.
Currently, no other lowest-level objects besides strings and lists are provided by the core engine. There is however a possibility to link externally compiled code that will provide additional types of objects. Those will be available in Yacas as "generic objects."
Evaluation when performed on an atom goes as follows: if the atom is bound locally as a variable, the object it is bound to is returned, otherwise, if it is bound as a global variable that is returned. Otherwise, the atom is returned unevaluated.
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. The engine first tries to find out if it is a built-in command (core function). In that case, the function is executed. Otherwise, it could be a user-defined function (with a "rule database"), and in that case the rules from the database are applied to it. If none of the rules are applicable, or if no rules are defined for it, the object is returned unevaluated.
The main properties of this scheme are the following. When objects are assigned to variables, they generally are evaluated (except if you are using the Hold() function) because assignment var := value is really a function call to Set(var, value) and this function evaluates its second argument (but not its first argument). When referencing that variable again, the object which is its value will not be re-evaluated. Also, the default behaviour of the engine is to return the original expression if it couldn't be evaluated. This is a desired behaviour if evaluation is used for simplifying expressions.
One major design flaw in Yacas (one that 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. In this case it might be useful to use the functions LocalSymbols and TemplateFunction. Calling
LocalSymbols(a,b) a+b; |
A function is identified by its name as returned by Type and the number of arguments, or "arity". The same name can be used with different arities to define different functions: f(x) is said to 'have arity 1' and f(x,y) has arity 2. Each of these functions may possess its own set of specific rules, which we shall call a "rule database" of a function.
Each function should be first declared with the built-in command RuleBase as follows:
RuleBase("FunctionName",{argument list}); |
In> f(a):=g(a)+1; Out> True; In> f(B); Out> g(a)+1; In> g(1+1); Out> g(1+1); In> RuleBase("g",{x}); Out> True; In> f(B); Out> g(B)+1; In> g(1+1); Out> g(2); |
The shorthand operator := for creating user functions that we illustrated in the tutorial is actually defined in the scripts and it makes the requisite call to the RuleBase function. After a RuleBase call you can specify parsing properties for the function if you like; for example, you could make it an infix or bodied operator.
Now we can add some rules to the rule database for a function. A rule simply states that if a specific function object with a specific arity is encountered in an expression and if a certain predicate is true, then Yacas should replace this function with some other expression. To tell Yacas about a new rule you can use the built-in Rule command. This command is what does the real work for the somewhat more aesthetically pleasing ... # ... <-- ... ; construct we have seen in the tutorial. Incidentally, you do not have to call RuleBase explicitly if you use that construct.
Here is the general syntax for a Rule call:
Rule("foo", arity, precedence, predicate) body; |
All rules for a given function can be erased with a call to TryRetract(funcname, arity). This is useful, for instance, when too many rules have been entered in the interactive mode. This call undefines the function and also invalidates the RuleBase declaration.
You can specify that function 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". Here the argument name "a" should be the same as that used in the RuleBase call when declaring these functions. Inhibiting evaluation of certain arguments 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.
Rule-based programming normally makes heavy use of recursion and it is important to control the order in which replacement rules are to be applied. For this purpose, each rule is given a precedence. Precedences go from low to high, so all rules with precedence 0 will be tried before any rule with precedence 1.
You can assign several rules to one and the same function, as long as some of the predicates differ. 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 a desired behaviour for symbolic manipulation. You can slowly build up your own functions, incrementally testing their properties.
In> RuleBase("f",{n}); |
The Rule commands in this example specified two rules for function "f" with arity 1: one rule with precedence 10 and predicate n=0, and another with precedence 20 and the predicate that returns True only if "n" is a positive integer. Rules with lowest precedence get evaluated first, so the rule with precedence 10 will be tried before the rule with precedence 20. Note that the predicates and the body use the name "n" declared by the RuleBase call.
After declaring RuleBase for a function, you could tell the parser to treat this function as a postfix operator:
In> Postfix("f"); |
Function ("FirstOf", {list}) list[ 1 ] ; |
Function("FirstOf", {list}) |
Finally, the function FirstOf could also have been defined by typing
FirstOf(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.
While (i < 10) |
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 \" .
You can tell the interpreter that a function can see the local variables from the calling environment by declaring UnFence(funcname, arity) on the specified function.