Tutorial¶
Yacas syntax¶
Expressions in Yacas are generally built up of words. We will not bore
you with the exact definitions of such words, but roughly speaking
they are either sequences of alphabetic letters, or a number, or a
bracket, or space to separate words, or a word built up from symbols
like +
, -
, *
, <
, etc.. If you want, you can mix
these different types of characters, by surrounding them with
quotes. Thus, "This text"
is what is called one token, surrounded
by quotes.
The usual notation people use when writing down a calculation is
called the infix notation, and you can readily recognize it, as for
example 2+3
and 3*4
. Prefix operators also exist. These
operators come before an expression, like for example the unary minus
sign (called unary because it accepts one argument), -(3*4)
. In
addition to prefix operators there are also postfix operators, like
the exclamation mark to calculate the factorial of a number, 10!
.
Yacas understands standard simple arithmetic expressions. Some examples:
2+3
(addition)2*3
(multiplication)2-3
(subtraction)2^3
(raising powers)2+3*4
(2+3)*4
6/3
(division)1/3
Divisions are not reduced to real numbers, but kept as a rational for
as long as possible, since the rational is an exact correct expression
(and any real number would just be an approximation). Yacas is able to
change a rational in to a number with the function N
, for example
N(1/3)
.
Operators have precedence, meaning that certain operations are done
first before others are done. For example, in 2+3*4
the
multiplication is performed before the addition. The usual way to
change the order of a calculation is with round brackets. The round
brackets in the expression (2+3)*4
will force Yacas to first add 2
and 3, and then multiply the result.
Simple function calls have their arguments between round brackets,
separated by commas. Examples are Sin(Pi)
(which indicates that
you are interested in the value of the trigonometric function
\(\sin\) applied to the constant \(\pi\)), and
Min(5,1,3,-5,10)
(which should return the lowest of its arguments,
-5
in this case). Functions usually have the form f()
,
f(x)
or f(x,y,z,...)
depending on how many arguments the
function accepts. Functions always return a result. For example,
Cos(0)
should return 1
. Evaluating functions can be thought of
as simplifying an expression as much as possible. Sometimes further
simplification is not possible and a function returns itself
unsimplified, like taking the square root of an integer Sqrt(2)
. A
reduction to a number would be an approximation. We explain elsewhere
how to get Yacas to simplify an expression to a number.
Yacas allows for use of the infix notation, but with some
additions. Functions can be bodied, meaning that the last argument
is written past the close bracket. An example is ForEach
, where we
write ForEach(item, 1 .. 10) Echo(item);
. Echo(item)
is the
last argument to the function ForEach
.
A list is enclosed with curly braces, and is written out with commas between the
elements, like for example {1,2,3}
. items in lists (and things
that can be made to look like lists, like arrays and strings), can
then be accessed by indicating the index between square brackets after
the object. {a,b,c}[2]
should return b
, as b
is the second
element in the list (Yacas starts counting from 1 when accessing
elements). The same can be done with strings: "abc"[2]
.
And finally, function calls can be grouped together, where they get
executed one at a time, and the result of executing the last
expression is returned. This is done through square brackets, as [
Echo("Hello"); Echo("World"); True; ];
, which first writes Hello
to screen, then World
on the next line, and then returns True
.
When you type in an expression, you have to take in to account the
fact that Yacas is case-sensitive. This means that a function sin
(with all lowercase) is a different function from Sin
(which
starts with a capital S), and the variable v
is a different one
from V
.
Using Yacas from the calculation center¶
As mentioned earlier, you can type in commands on the command line in
the calculation center. Typically, you would enter one statement per
line, for example, click on Sin(Pi/2);
. The has a memory, and
remembers results from calculations performed before. For example, if
you define a function on a line (or set a variable to a value), the
defined function (or variable) are available to be used in following
lines. A session can be restarted (forgetting all previous definitions
and results) by typing restart
. All memory is erased in that
case.
Statements should end with a semicolon ;
although this is not
required in interactive sessions (Yacas will append a semicolon at end
of line to finish the statement).
The command line has a history list, so it should be easy to browse through the expressions you entered previously using the up and down arrow keys.
When a few characters have been typed, the command line will use the characters before the cursor as a filter into the history, and allow you to browse through all the commands in the history that start with these characters quickly, instead of browsing through the entire history. If the system recognized the first few characters, it will also show the commands that start with the sequence entered. You can use the arrow keys to browse through this list, and then select the intended function to be inserted by pressing enter.
Commands spanning multiple lines can (and actually have to) be entered
by using a trailing backslash at end of each continued line. For
example, clicking on 2+3+
will result in an
error, but entering the same with a backslash at the end and then
entering another expression will concatenate the two lines and
evaluate the concatenated input.
Incidentally, any text Yacas prints without a prompt is either
a message printed by a function as a side-effect, or an error
message. Resulting values of expressions are always printed after an
Out>
prompt.
Yacas as a symbolic calculator¶
We are ready to try some calculations. Yacas uses a C-like
infix syntax and is case-sensitive. Here are some exact manipulations
with fractions for a start: 1/14+5/21*(30-(1+1/2)*5^2);
The standard scripts already contain a simple math library for
symbolic simplification of basic algebraic functions. Any names such
as x
are treated as independent, symbolic variables and are not
evaluated by default. Some examples to try:
0+x
x+1*y
Sin(ArcSin(alpha))+Tan(ArcTan(beta))
Note that the answers are not just simple numbers here, but actual expressions. This is where Yacas shines. It was built specifically to do calculations that have expressions as answers.
In Yacas after a calculation is done, you can refer to the previous
result with %
. For example, we could first type (x+1)*(x-1)
,
and then decide we would like to see a simpler version of that
expression, and thus type Simplify(%)
,
which should result in x^2-1
.
The special operator %
automatically recalls the result from the
previous line. The function Simplify
attempts to reduce an
expression to a simpler form. Note that standard function names in
Yacas are typically capitalized. Multiple capitalization such as
ArcSin
is sometimes used. The underscore character _
is a
reserved operator symbol and cannot be part of variable or function
names.
Yacas offers some more powerful symbolic manipulation operations. A few will be shown here to wetten the appetite.
Some simple equation solving algorithms are in place:
Solve(x/(1+x) == a, x);
Solve(x^2+x == 0, x);
Solve(a+x*y==z,x);
(Note the use of the ==
operator, which does not evaluate to
anything, to denote an “equation” object.)
Taylor series are supported, for example: Taylor(x,0,3) Exp(x)
is
a bodied operator that expands Exp(x)
for x
around x=0
, up
to order 3.
Symbolic manipulation is the main application of Yacas. This is a small tour of the capabilities Yacas currently offers. Note that this list of examples is far from complete. Yacas contains a few hundred commands, of which only a few are shown here.
Expand((1+x)^5);
(expand the expression into a polynomial)Limit(x,0) Sin(x)/x;
(calculate the limit ofSin(x)/x
asx
approaches zero)Newton(Sin(x),x,3,0.0001);
(use Newton’s method to find the value ofx
near3
whereSin(x)
equals zero numerically and stop if the result is closer than0.0001
to the real result)DiagonalMatrix({a,b,c});
(create a matrix with the elements specified in the vector on the diagonal)Integrate(x,a,b) x*Sin(x);
(integrate a function over variablex
, froma
tob
)Factor(x^2-1);
(factorize a polynomial)Apart(1/(x^2-1),x);
(create a partial fraction expansion of a polynomial)Simplify((x^2-1)/(x-1));
(simplification of expressions)CanProve( (a And b) Or (a And Not b) );
(special-purpose simplifier that tries to simplify boolean expressions as much as possible)TrigSimpCombine(Cos(a)*Sin(b));
(special-purpose simplifier that tries to transform trigonometric expressions into a form where there are only additions of trigonometric functions involved and no multiplications)
Arbitrary precision numbers¶
Yacas can deal with arbitrary precision numbers. It can work with
large integers, like 20!
(The ! means factorial, thus
1*2*3*...*20
).
As we saw before, rational numbers will stay rational as long as the
numerator and denominator are integers, so 55/10
will evaluate to
11/2
. You can override this behavior by using the numerical
evaluation function N()
. For example, N(55/10)
will evaluate
to 5.5
. This behavior holds for most math functions. Yacas will
try to maintain an exact answer (in terms of integers or fractions)
instead of using floating point numbers, unless N()
is used. Where
the value for the constant pi is needed, use the built-in variable
Pi
. It will be replaced by the (approximate) numerical value when
N(Pi)
is called. Yacas knows some simplification rules using
Pi
(especially with trigonometric functions).
The function N
takes either one or two arguments. It evaluates its
first argument and tries to reduce it as much as possible to a
real-valued approximation of the expression. If the second argument is
present, it states the number of digits precision required. Thus
N(1/234)
returns a number with the current default precision
(which starts at 20 digits), but you can request as many digits as you
like by passing a second argument, as in N(1/234, 10)
, N(1/234,
20)
, N(1/234, 30)
, etcetera.
Note that we need to enter N()
to force the approximate
calculation, otherwise the fraction would have been left unevaluated.
Revisiting Pi
, we can get as many digits of Pi
as we like, by
providing the precision required as argument to N
. So to get 50
digits precision, we can evaluate N(Pi,50)
.
Taking a derivative of a function was amongst the very first of
symbolic calculations to be performed by a computer, as the operation
lends itself surprisingly well to being performed
automatically. Naturally, it is also implemented in Yacas, through the
function D
. D
is a bodied function, meaning that its
last argument is past the closing brackets. Where normal functions are
called with syntax similar to f(x,y,z)
, a bodied function would be
called with a syntax f(x,y)z
. Here are two examples of taking a
derivative:
D(x) Sin(x);
(taking a derivative)D(x) D(x) Sin(x);
(taking a derivative twice)
The D()
function also accepts an argument specifying how many times the
derivative has to be taken. In that case, the above expressions can also be
written as:
D(x,1) Sin(x);
(taking a derivative)D(x,2) Sin(x);
(taking a derivative twice)
Analytic functions¶
Many of the usual analytic functions have been defined in the yacas library.
Examples are Exp(1)
, Sin(2)
, ArcSin(1/2)
, Sqrt(2)
. These will
not evaluate to a numeric result in general, unless the result is an integer,
like Sqrt(4)
. If asked to reduce the result to a numeric approximation with
the function N()
, then yacas will do so, as for example in
N(Sqrt(2),50)
.
Variables¶
Yacas supports variables. You can set the value of a variable with the
:=
infix operator, as in a:=1;
. The variable can then be used
in expressions, and everywhere where it is referred to, it will be
replaced by its value.
To clear a variable binding, execute Clear(a);
. A variable will
evaluate to itself after a call to clear it (so after the call to
clear a
above, calling <span class=”commandlink”>a`` should now
return a
). This is one of the properties of the evaluation scheme
of Yacas; when some object can not be evaluated or transformed any
further, it is returned as the final result.
Functions¶
The :=
operator can also be used to define simple functions:
f(x):=2*x*x
. will define a new function, f
, that accepts one
argument and returns twice the square of that argument. This function
can now be called, f(a)
. You can change the definition of a
function by defining it again.
One and the same function name such as f
may define different
functions if they take different numbers of arguments. One can define
a function f
which takes one argument, as for example
f(x):=x^2;
, or two arguments, f(x,y):=x*y;
. If you clicked on
both links, both functions should now be defined, and f(a)
calls
the one function whereas f(a,b)
calls the other.
Yacas is very flexible when it comes to types of mathematical objects. Functions can in general accept or return any type of argument.
Boolean expressions and predicates¶
Yacas predefines True
and False
as boolean values. Functions
returning boolean values are called predicates. For example, IsNumber()
and IsInteger()
are predicates defined in the yacas environment. For
example, try IsNumber(2+x)
, or IsInteger(15/5)
.
There are also comparison operators. Typing 2 > 1
would return True
.
You can also use the infix operators And
and Or
, and the prefix operator
Not
, to make more complex boolean expressions. For example, try True And
False
, True Or False
, True And Not(False)
.
Strings and lists¶
In addition to numbers and variables, Yacas supports strings and
lists. Strings are simply sequences of characters enclosed by double
quotes, for example: "this is a string with \"quotes\" in it"
.
Lists are ordered groups of items, as usual. Yacas represents lists by
putting the objects between braces and separating them with
commas. The list consisting of objects a, b, and c could be entered by
typing {a,b,c}
. In Yacas, vectors are represented as lists and
matrices as lists of lists.
Items in a list can be accessed through the [ ]
operator. The
first element has index one. Examples: when you enter
uu:={a,b,c,d,e,f};
then uu[2];
evaluates to b
, and
uu[2 .. 4];
evaluates to {b,c,d}
. The “range” expression
2 .. 4
evaluates to {2,3,4}
. Note that spaces around the
..
operator are necessary, or else the parser will not be able to
distinguish it from a part of a number.
Lists evaluate their arguments, and return a list with results of
evaluating each element. So, typing {1+2,3};
would evaluate to {3,3}
.
The idea of using lists to represent expressions dates back to the language LISP developed in the 1970’s. From a small set of operations on lists, very powerful symbolic manipulation algorithms can be built. Lists can also be used as function arguments when a variable number of arguments are necessary.
Let’s try some list operations now. First click on m:={a,b,c};
to
set up an initial list to work on. Then click on links below:
Length(m)
(return the length of a list)Reverse(m)
(return the string reversed)Concat(m,m)
(concatenate two strings)m[1]:=d
(setting the first element of the list to a new value,d
, as can be verified by evaluatingm
)
Many more list operations are described in the reference manual.
Writing simplification rules¶
Mathematical calculations require versatile transformations on symbolic quantities. Instead of trying to define all possible transformations, Yacas provides a simple and easy to use pattern matching scheme for manipulating expressions according to user-defined rules. Yacas itself is designed as a small core engine executing a large library of rules to match and replace patterns.
One simple application of pattern-matching rules is to define new
functions. (This is actually the only way Yacas can learn about new
functions.) As an example, let’s define a function f
that will
evaluate factorials of non-negative integers. We will define a
predicate to check whether our argument is indeed a non-negative
integer, and we will use this predicate and the obvious recursion
f(n)=n*f(n-1) if n>0 and 1 if n=0
to evaluate the factorial.
We start with the simple termination condition, which is that f(n)
should return one if n
is zero: 10 # f(0) <-- 1;
. You can verify
that this already works for input value zero, with f(0)
.
Now we come to the more complex line 20 # f(n_IsIntegerGreaterThanZero) <--
n*f(n-1);
Now we realize we need a function IsGreaterThanZero()
, so we
define this function, with IsIntegerGreaterThanZero(_n) <-- (IsInteger(n) And
n > 0);
You can verify that it works by trying f(5)
, which should return
the same value as 5!
.
In the above example we have first defined two simplification rules
for a new function f()
. Then we realized that we need to define a
predicate IsIntegerGreaterThanZero()
. A predicate equivalent to
IsIntegerGreaterThanZero()
is actually already defined in the
standard library and it’s called IsPositiveInteger()
, so it was not
necessary, strictly speaking, to define our own predicate to do the
same thing. We did it here just for illustration purposes.
The first two lines recursively define a factorial function \(f(n)=n(n-1)\ldots 1\). The rules are given precedence values 10 and 20, so the first rule will be applied first. Incidentally, the factorial is also defined in the standard library as a postfix operator ! and it is bound to an internal routine much faster than the recursion in our example. The example does show how to create your own routine with a few lines of code. One of the design goals of Yacas was to allow precisely that, definition of a new function with very little effort.
The operator <--
defines a rule to be applied to a specific
function. (The <--
operation cannot be applied to an atom.)
The _n
in the rule for IsIntegerGreaterThanZero()
specifies
that any object which happens to be the argument of that predicate is
matched and assigned to the local variable n
. The expression to
the right of <--
can use n
(without the underscore) as a
variable.
Now we consider the rules for the function f()
. The first rule just
specifies that f(0)
should be replaced by 1 in any expression. The
second rule is a little more involved. n_IsIntegerGreaterThanZero
is a match for the argument of f
, with the proviso that the
predicate IsIntegerGreaterThanZero(n)
should return True
,
otherwise the pattern is not matched. The underscore operator is to be
used only on the left hand side of the rule definition operator
<--
.
There is another, slightly longer but equivalent way of writing the second rule:
20 # f(_n)_(IsIntegerGreaterThanZero(n)) <-- n*f(n-1);
The underscore after
the function object denotes a postpredicate that should return True
or
else there is no match. This predicate may be a complicated expression involving
several logical operations, unlike the simple checking of just one predicate in
the n_IsIntegerGreaterThanZero
construct. The postpredicate can also use the
variable n
(without the underscore).
Precedence values for rules are given by a number followed by the
#
infix operator (and the transformation rule after it). This
number determines the ordering of precedence for the pattern matching
rules, with 0 the lowest allowed precedence value, i.e. rules with
precedence 0 will be tried first. Multiple rules can have the same
number: this just means that it doesn’t matter what order these
patterns are tried in. If no number is supplied, 0 is assumed. In our
example, the rule f(0) <-- 1
must be applied earlier than the
recursive rule, or else the recursion will never terminate. But as
long as there are no other rules concerning the function f
, the
assignment of numbers 10 and 20 is arbitrary, and they could have been
500 and 501 just as well. It is usually a good idea however to keep
some space between these numbers, so you have room to insert new
transformation rules later on.
Predicates can be combined: for example, IsIntegerGreaterThanZero()
could also have been defined as:
10 # IsIntegerGreaterThanZero(n_IsInteger)_(n>0) <-- True;
20 # IsIntegerGreaterThanZero(_n) <-- False;
The first rule specifies that if n
is an integer, and is greater than
zero, the result is True
, and the second rule states that
otherwise (when the rule with precedence 10 did not apply) the
predicate returns False
.
In the above example, the expression n > 0
is added after the
pattern and allows the pattern to match only if this predicate return
True
. This is a useful syntax for defining rules with complicated
predicates. There is no difference between the rules
F(n_IsPositiveInteger) <-- ...
and F(_n)_(IsPositiveInteger(n))
<-- ...
except that the first syntax is a little more concise.
The rule expression has the following form:
[precedence #] pattern [_ postpredicate] <-- replacement;
The optional precedence must be a positive integer.
Some more examples of rules (not made clickable because their equivalents are already in the basic yacas library):
10 # _x + 0 <-- x;
20 # _x - _x <-- 0;
ArcSin(Sin(_x)) <-- x;
The last rule has no explicit precedence specified in it (the precedence zero will be assigned automatically by the system).
Yacas will first try to match the pattern as a template. Names
preceded or followed by an underscore can match any one object: a
number, a function, a list, etc. Yacas will assign the relevant
variables as local variables within the rule, and try the predicates
as stated in the pattern. The post-predicate (defined after the
pattern) is tried after all these matched. As an example, the
simplification rule _x - _x <--0
specifies that the two objects
at left and at right of the minus sign should be the same for this
transformation rule to apply.
Local simplification rules¶
Sometimes you have an expression, and you want to use specific
simplification rules on it that should not be universally applied.
This can be done with the /:
and the /::
operators. Suppose
we have the expression containing things such as Ln(a*b)
, and we
want to change these into Ln(a)+Ln(b)
. The easiest way to do this
is using the /:
operator as follows:
Sin(x)*Ln(a*b)
(example expression without simplification)Sin(x)*Ln(a*b) /: {Ln(_x*_y) <- Ln(x)+Ln(y) }
(with instruction to simplify the expression)
A whole list of simplification rules can be built up in the list, and
they will be applied to the expression on the left hand side of
/:
.
Note that for these local rules, <-
should be used instead of
<--
. Using latter would result in a global definition of a new
transformation rule on evaluation, which is not the intention.
The /:
operator traverses an expression from the top down, trying
to apply the rules from the beginning of the list of rules to the end
of the list of rules. If no rules can be applied to the whole
expression, it will try the sub-expressions of the expression being
analyzed.
It might be sometimes necessary to use the /::
operator, which
repeatedly applies the /:
operator until the result does not
change any more. Caution is required, since rules can contradict each
other, and that could result in an infinite loop. To detect this
situation, just use /:
repeatedly on the expression. The
repetitive nature should become apparent.
Programming essentials¶
An important feature of yacas is its programming language which allows you to create your own programs for doing calculations. This section describes some constructs and functions for control flow.
Looping can be done with the function ForEach()
. There are more
options, but ForEach()
is the simplest to use for now and will suffice
for this turorial. The statement form ForEach(x, list) body
executes its body for each element of the list and assigns the
variable x
to that element each time. The statement form
While(predicate) body
repeats execution of the expression
represented by body
until evaluation of the expression represented
by predicate
returns False
.
This example loops over the integers from one to three, and writes out
a line for each, multiplying the integer by 3 and displaying the
result with the function Echo()
:
ForEach(x,1 .. 5) Echo(x," times 3 equals ",3*x);
Compound statements¶
Multiple statements can be grouped together using the [
and ]
brackets. The compound [a; Echo("In the middle"); 1+2;];
evaluates
a
, then the Echo()
command, and finally evaluates 1+2
, and
returns the result of evaluating the last statement 1+2
.
A variable can be declared local to a compound statement block by the
function Local(var1, var2, ...)
. For example, if you execute
[Local(v);v:=1+2;v;];
the result will be 3
. The program body
created a variable called v
, assigned the value of evaluating
1+2
to it, and made sure the contents of the variable v
were
returned. If you now evaluate v
afterwards you will notice that
the variable v
is not bound to a value any more. The variable
v
was defined locally in the program body between the two square
brackets [
and ]
.
Conditional execution is implemented by the
If(predicate, body1, body2)
function call. If the expression predicate
evaluates to
True
, the expression represented by body1
is evaluated,
otherwise body2
is evaluated, and the corresponding value is
returned. For example, the absolute value of a number can be computed
with: f(x) := If(x < 0,-x,x);
(note that there already is a
standard library function that calculates the absolute value of a
number).
Variables can also be made to be local to a small set of functions,
with LocalSymbols(variables) body
. For example, the following code
snippet:
LocalSymbols(a,b) [
a:=0;
b:=0;
inc():=[a:=a+1;b:=b-1;show();];
show():=Echo("a = ",a," b = ",b);
];
defines two functions, inc()
and show()
. Calling inc()
repeatedly increments a
and decrements b
, and calling
show()
then shows the result (the function inc()
also calls the
function show()
, but the purpose of this example is to show how two
functions can share the same variable while the outside world cannot
get at that variable). The variables are local to these two functions,
as you can see by evaluating a
and b
outside the scope of
these two functions. This feature is very important when writing a
larger body of code, where you want to be able to guarantee that there
are no unintended side-effects due to two bits of code defined in
different files accidentally using the same global variable.
To illustrate these features, let us create a list of all even integers from 2 to 20 and compute the product of all those integers except those divisible by 3:
[
Local(L,i,answer);
L:={}; i:=2;
/*Make a list of all even integers from 2 to 20 */
While (i <= 20) [ L := Append(L, i); i := i + 2; ];
/* Now calculate the product of all of these numbers that are not divisible by 3 */
answer := 1;
ForEach(i,L) If (Mod(i, 3) != 0, answer := answer * i);
/* And return the answer */
answer;
];
(Note that it is not necessarily the most economical way to do it in yacas.)
We used a shorter form of If(predicate, body)
with only one body
which is executed when the condition holds. If the condition does not
hold, this function call returns False
. We also introduced
comments, which can be placed between /*
and */
. Yacas will
ignore anything between those two. When putting a program in a file
you can also use //
. Everything after //
up until the end of
the line will be a comment. Also shown is the use of the While
function. Its form is While (predicate) body
. While the
expression represented by predicate
evaluates to True
, the
expression represented by body
will keep on being evaluated.
The above example is not the shortest possible way to write out the algorithm. It is written out in a procedural way, where the program explains step by step what the computer should do. There is nothing fundamentally wrong with the approach of writing down a program in a procedural way, but the symbolic nature of Yacas also allows you to write it in a more concise, elegant, compact way, by combining function calls.
There is nothing wrong with procedural style, but there is amore ‘functional’ approach to the same problem would go as follows below. The advantage of the functional approach is that it is shorter and more concise (the difference is cosmetic mostly).
Before we show how to do the same calculation in a functional style,
we need to explain what a pure function is, as you will need it a
lot when programming in a functional style. We will jump in with an
example that should be self-explanatory. Consider the expression
Lambda({x,y},x+y)
. This has two arguments, the first listing x
and y
, and the second an expression. We can use this construct with
the function Apply()
as follows:
Apply(Lambda({x,y},x+y),{2,3})
The result should be 5
, the result of adding 2
and 3
. The
expression starting with Lambda()
is essentially a prescription for
a specific operation, where it is stated that it accepts 2 arguments,
and returns the arguments added together. In this case, since the
operation was so simple, we could also have used the name of a
function to apply the arguments to, the addition operator in this case
Apply("+",{2,3})
. When the operations become more complex however,
the Lambda()
construct becomes more useful.
Now we are ready to do the same example using a functional
approach. First, let us construct a list with all even numbers from 2
to 20. For this we use the ..
operator to set up all numbers from
one to ten, and then multiply that with two: 2 * (1 .. 10)
.
Now we want an expression that returns all the even numbers up to 20
which are not divisible by 3. For this we can use Select
, which
takes as first argument a predicate that should return True
if the
list item is to be accepted, and False
otherwise, and as second
argument the list in question:
Select(Lambda({n},Mod(n,3)!=0),2*(1 .. 10))
The numbers 6, 12 and 18 have been correctly filtered out. Here you see one example of a pure function where the operation is a little bit more complex.
All that remains is to factor the items in this list. For this we can
use UnFlatten
. Two examples of the use of UnFlatten
are
UnFlatten({a,b,c},"*",1)
UnFlatten({a,b,c},"+",0)
The 0 and 1 are a base element to start with when grouping the arguments in to an expression (they should be the respective identity elements, hence it is zero for addition and 1 for multiplication).
Now we have all the ingredients to finally do the same calculation we did above in a procedural way, but this time we can do it in a functional style, and thus captured in one concise single line:
UnFlatten(Select(Lambda({n},Mod(n,3)!=0),2*(1 .. 10)),"*",1)
As was mentioned before, the choice between the two is mostly a matter of style.
Macros¶
One of the powerful constructs in yacas is the construct of a macro. In its essence, a macro is a prescription to create another program before executing the program. An example perhaps explains it best. Evaluate the following expression
Macro(for,{st,pr,in,bd}) [(@st);While(@pr)[(@bd);(@in);];];
This expression defines a macro that allows for looping. Yacas has a
For()
function already, but this is how it could be defined in one line
(In yacas the For()
function is bodied, we left that out here for clarity,
as the example is about macros).
To see it work just type for(i:=0,i<3,i:=i+1,Echo(i))
. You will see
the count from one to three.
The construct works as follows; The expression defining the macro sets
up a macro named for()
with four arguments. On the right is the body
of the macro. This body contains expressions of the form @var
.
These are replaced by the values passed in on calling the macro.
After all the variables have been replaced, the resulting expression
is evaluated. In effect a new program has been created. Such macro constructs
come from LISP, and are famous for allowing you to almost design your own
programming language constructs just for your own problem at hand. When used
right, macros can greatly simplify the task of writing a program.
You can also use the back-quote `
to expand a macro in-place. It
takes on the form `(expression)
, where the expression can again
contain sub-expressions of the form @variable
. These instances
will be replaced with the values of these variables.
The practice of programming in yacas¶
When you become more proficient in working with yacas you will be
doing more and more sophisticated calculations. For such calculations
it is generally necessary to write little programs. In real life you
will usually write these programs in a text editor, and then start
yacas, load the text file you just wrote, and try out the
calculation. Generally this is an iterative process, where you go back
to the text editor to modify something, and then go back to yacas,
type restart
and then reload the file.
On this site you can run yacas in a little window called a yacas calculation center (the same as the one below this tutorial). On page there is tab that contains a Yacas calculation center. If you click on that tab you will be directed to a larger calculation center than the one below this tutorial. In this page you can easily switch between doing a calculation and editing a program to load at startup. We tried to make the experience match the general use of Yacas on a desktop as much as possible. The Yacas journal (which you see when you go to the Yacas web site) contains examples of calculations done before by others.
Defining your own operators¶
Large part of the yacas system is defined in the scripting language itself. This includes the definitions of the operators it accepts, and their precedences. This means that you too can define your own operators. This section shows you how to do that.
Suppose we wanted to define a function F(x,y)=x/y+y/x
. We could
use the standard syntax F(a,b) := a/b + b/a;
. F(1,2);
. For
the purpose of this demonstration, lets assume that we want to define
an infix operator xx
for this operation. We can teach yacas about
this infix operator with Infix("xx", OpPrecedence("/"));
. Here we
told Yacas that the operator xx
is to have the same precedence as
the division operator. We can now proceed to tell Yacas how to
evaluate expressions involving the operator xx
by defining it as
we would with a function, a xx b := a/b + b/a;
.
You can verify for yourself 3 xx 2 + 1;
and 1 + 3 xx 2;
return
the same value, and that they follow the precedence rules (eg. xx
binds stronger than +
).
We have chosen the name xx
just to show that we don’t need to use
the special characters in the infix operator’s name. However we must
define this operator as infix before using it in expressions,
otherwise yacas will raise a syntax error.
Finally, we might decide to be completely flexible with this important
function and also define it as a mathematical operator ##
. First
we define ##
as a bodied function and then proceed as
before. First we can tell yacas that ##
is a bodied operator with
Bodied("##", OpPrecedence("/"));
. Then we define the function
itself: ##(a) b := a xx b;
. And now we can use the function,
##(1) 3 + 2;
.
We have used the name ##
but we could have used any other name
such as xx
or F
or even _-+@+-_
. Apart from possibly
confusing yourself, it doesn’t matter what you call the functions you
define.
There is currently one limitation in yacas: once a function name is
declared as infix (prefix, postfix) or bodied, it will always be
interpreted that way. If we declare a function f
to be bodied, we
may later define different functions named f
with different
numbers of arguments, however all of these functions must be bodied.
When you use infix operators and either a prefix of postfix operator next to it you can run in to a situation where yacas can not quite figure out what you typed. This happens when the operators are right next to each other and all consist of symbols (and could thus in principle form a single operator). Yacas will raise an error in that case. This can be avoided by inserting spaces.
Some assorted programming topics¶
One use of lists is the associative list, sometimes called a
dictionary in other programming languages, which is implemented in
Yacas simply as a list of key-value pairs. Keys must be strings and
values may be any objects. Associative lists can also work as
mini-databases, where a name is associated to an object. As an
example, first enter record:={};
to set up
an empty record. After that, we can fill arbitrary fields in this
record:
record["name"]:="Isaia";
record["occupation"]:="prophet";
record["is alive"]:=False;
Now, evaluating record["name"]
should result in the answer
"Isaia"
. The record is now a list that contains three sublists, as
you can see by evaluating record
.
Assignment of multiple variables is also possible using lists. For
instance, evaluating {x,y}:={2!,3!}
will result in 2 being
assigned to x
and 6 to y
.
When assigning variables, the right hand side is evaluated before it
is assigned. Thus a:=2*2
will set a to 4. This is however
not the case for functions. When entering f(x):=x+x
the
right hand side, x+x
, is not evaluated before being assigned. This
can be forced by using Eval()
. Defining f(x)
with
f(x):=Eval(x+x)
will tell the system to first evaluate x+x
(which results in 2*x
) before assigning it to the user function
f()
. This specific example is not a very useful one but it will come
in handy when the operation being performed on the right hand side is
expensive. For example, if we evaluate a Taylor series expansion
before assigning it to the user-defined function, the engine doesn’t
need to create the Taylor series expansion each time that user-defined
function is called.
The imaginary unit \(\imath\) is denoted I
and complex numbers can be
entered as either expressions involving I
, as for example
1+I*2
, or explicitly as Complex(a,b)
for \(a+\imath b\). The form
Complex(re,im)
is the way yacas deals with complex numbers
internally.
Linear Algebra¶
Vectors of fixed dimension are represented as lists of their
components. The list {1, 2+x, 3*Sin(p)}
would be a
three-dimensional vector with components 1
, 2+x
and
3*Sin(p)
. Matrices are represented as a lists of lists.
Vector components can be assigned values just like list items, since
they are in fact list items. If we first set up a variable called
“vector” to contain a three-dimensional vector with the command
vector:=ZeroVector(3);
(you can verify that it is indeed a vector
with all components set to zero by evaluating vector
), you can
change elements of the vector just like you would the elements of a
list (seeing as it is represented as a list). For example, to set the
second element to two, just evaluate vector[2] := 2;
. This results
in a new value for vector
.
Yacas can perform multiplication of matrices, vectors and numbers as
usual in linear algebra. The standard Yacas script library also
includes taking the determinant and inverse of a matrix, finding
eigenvectors and eigenvalues (in simple cases) and solving linear sets
of equations, such as A * x = b where A is a matrix, and x and b are
vectors. As a little example to wetten your appetite, we define a
Hilbert matrix: hilbert:=HilbertMatrix(3)
. We can then calculate
the determinant with Determinant(hilbert)
, or the inverse with
Inverse(hilbert)
. There are several more matrix operations
supported. See the reference manual for more details.
Threading of functions¶
Some functions in Yacas can be threaded. This means that calling the
function with a list as argument will result in a list with that
function being called on each item in the list. E.g. Sin({a,b,c});
will result in {Sin(a),Sin(b),Sin(c)}
. This functionality is
implemented for most normal analytic functions and arithmetic
operators.
Functions as lists¶
For some work it pays to understand how things work under the
hood. Internally, Yacas represents all atomic expressions (numbers and
variables) as strings and all compound expressions as lists, like
Lisp. Try FullForm(a+b*c);
and you will see the text (+ a (* b c
))
appear on the screen. This function is occasionally useful, for
example when trying to figure out why a specific transformation rule
does not work on a specific expression.
If you try FullForm(1+2)
you will see that the result is not quite
what we intended. The system first adds up one and two, and then shows
the tree structure of the end result, which is a simple number
3
. To stop Yacas from evaluating something, you can use the
function Hold
, as FullForm(Hold(1+2))
. The function Eval
is the opposite, it instructs Yacas to re-evaluate its argument
(effectively evaluating it twice). This undoes the effect of Hold
,
as for example Eval(Hold(1+2))
.
Also, any expression can be converted to a list by the function
Listify
or back to an expression by the function UnList
:
Listify(a+b*(c+d));
UnList({Atom("+"),x,1});
Note that the first element of the list is the name of the function
+
which is equivalently represented as Atom("+")
and that the
subexpression b*(c+d)
was not converted to list form. Listify just
took the top node of the expression.