In [1]:
# using Symata # when starting a stock Julia image
isymata() # when starting the precompiled-with Symata image

This is a translation from Mathematica to Symata of the Introduction to "Mathematica programming: an advanced introduction" v 1.01 by Leonid Shifrin. Minimal changes to accomodate Symata were made. This text is licensed under the Creative Commons Attribution-Noncomercial-Share Alike 3.0 United States license. http://creativecommons.org/licenses/by-nc-sa/3.0/us/

Mathematica is a registered trademark of Wolfram Research Inc.

Despite the title, since this is only the introduction, there is almost nothing about programming here. Rather it illustrates some of the core principles of the language. These principles concern the part of Symata that follows Mathematica semantics. Symata also breaks and enhances Mathematica semantics by integrating smoothly with Julia.

Introduction

First principle: everything is an expression

The first principle states that every object dealt with by Symata, is an expression. Every Symata expression is either Atom, or a Normal Expression.

Atoms and the built-in AtomQ predicate

Atoms are numbers, symbols and strings, and numbers are further divided into Integers, Reals, Rationals and Complex. All other objects are composite and are called Normal Expressions. It is always possible to check whether or not an expression is an atom or a composite, by acting on it with the built-in predicate AtomQ. For instance:

In [2]:
ClearAll()
[AtomQ(x), AtomQ(Sin(x)), AtomQ(1 + I * 2), AtomQ(2 / 3)]
Out[2]:
$$ \left[ \text{True},\text{False},\text{True},\text{True} \right] $$

Symata normal (composite) expressions

Every normal expression (composite) is built according to a universal pattern: expr[el1, ..., eln] Here it is required that some symbol <expr> is present (it can itself be a normal expression, not necessar- ily an atom), as well as the single square brackets. Inside the parentheses, there can be zero, one or several comma-separated elements <el1>,...,<eln>. These elements themselves can be either atoms or normal expressions. In an expression Sin(x), <expr> is Sin, and there is a single element <x>, which is atom (as long as x is not defined as something else, but this already has to do with expression evaluation and will be discussed below). It is clear that an arbitrary Symata expression must have a tree-like structure, with the branches being normal (sub)expressions and leaves being atoms.

Literal equivalents of built-in functions, and FullForm command

As a consequence, any built-in command/function in Symata has a literal/string equivalent (so that it can be represented in the above uniform way). This is most easily seen with the help of the built-in function FullForm, which shows the internal representation of any object/expression, in the way it is really "seen" by the kernel. For instance:

In [3]:
[z * Sin(x +y), FullForm(z * Sin(x +y))]
Out[3]:
$$ \left[ z \ \text{Sin} \! \left( x + y \right) ,\text{Times(z,Sin(Plus(x,y)))} \right] $$

The second expression in the square brackets is equivalent to the first one, but explicitly shows the structure described above.

All normal expressions are trees - TreeForm command (not implemented)

In the following example <expr> is Times (the multiplication command):

In [4]:
a = z * Sin(x +y);
FullForm(a)
Out[4]:
Times(z,Sin(Plus(x,y)))

Heads of expressions and the Head command

In general, an expression outside the square brackets has a name - it is called a head of expression, or just head. There is a built-in function with the same name, which allows to obtain the head of an arbitrary expression. For example:

In [5]:
Head(a)
Out[5]:
$$ \text{Times} $$

A head of an expression may be either an atom or a normal expression itself. For example :

In [6]:
Clear(b, f, g, h, x);
b = f(g)(h)(x);
Head(b)
Out[6]:
$$ f \! \left( g \right) \! \left( h \right) $$
In [7]:
Head(f(g)(h))
Out[7]:
$$ f \! \left( g \right) $$
In [8]:
Head(f(g))
Out[8]:
$$ f $$

Every expression has a head, even atoms. Heads for them include String, Symbol, Integer, Real, Rational and Complex. For instance :

In [9]:
[Head(f), Head(2), Head(Pi), Head(3.14), Head("abc"), Head(2/3), Head(1 + I)]
Out[9]:
$$ \left[ \text{Symbol},\text{Int64},\text{Symbol},\text{Float64},\text{String},\text{Rational{Int64}},\text{Complex{Int64}} \right] $$

Accessing individual parts of expressions through indexing

One can access also the internal parts of an expression (those inside the parentheses), by using indexing (Part command). The following example illustrates this.

In [10]:
[a[0], a[1], a[2], a[2, 0], a[2, 1], a[2, 1, 0], a[2, 1, 1], a[2, 1, 2]]
Out[10]:
$$ \left[ \text{Times},z,\text{Sin} \! \left( x + y \right) ,\text{Sin},x + y,\text{Plus},x,y \right] $$

We have just deconstructed our expression to pieces. In fact, we started from the "stem" and then went down along the "branches" to the "leaves" of the tree which we have seen above with the TreeForm. We see that the addresses (index sequences) which end with zero give the Heads of the subexpressions - this is a convention. In principle, any complex expression can be deconstructed in this way, and moreover, one can change its subexpressions.

Levels of expressions and the Level command

It is also possible to get access to the branches (subexpressions) which are at the certain distance (level) from the "stem". This is achieved by using a built-in Level command. Consider an example:

In [11]:
Clear(a)
a = z * Sin(x + y) + z1 * Cos(x1 + y1)
Out[11]:
$$ z1 \ \text{Cos} \! \left( x1 + y1 \right) + z \ \text{Sin} \! \left( x + y \right) $$

Here it is in its full form:

In [12]:
FullForm(a)
Out[12]:
Plus(Times(z1,Cos(Plus(x1,y1))),Times(z,Sin(Plus(x,y))))

These are the levels of the tree:

In [13]:
Level(a, [0])
Out[13]:
$$ \left[ z1 \ \text{Cos} \! \left( x1 + y1 \right) + z \ \text{Sin} \! \left( x + y \right) \right] $$
In [14]:
Level(a, [1])
Out[14]:
$$ \left[ z1 \ \text{Cos} \! \left( x1 + y1 \right) ,z \ \text{Sin} \! \left( x + y \right) \right] $$
In [15]:
Level(a, [2])
Out[15]:
$$ \left[ z1,\text{Cos} \! \left( x1 + y1 \right) ,z,\text{Sin} \! \left( x + y \right) \right] $$
In [16]:
Level(a, [3])
Out[16]:
$$ \left[ x1 + y1,x + y \right] $$
In [17]:
Level(a, [4])
Out[17]:
$$ \left[ x1,y1,x,y \right] $$

Level[a, {n}] gives all branches (or leaves) which have a distance of n levels down from the "stem". If however we need all branches that have n levels of sub - branches (or leaves), then we use a negative level Level[a, {-n}] :

In [18]:
Level(a, [-1])
Out[18]:
$$ \left[ x1,y1,x,y \right] $$
In [19]:
Level(a, [-2])
Out[19]:
$$ \left[ x1 + y1,x + y \right] $$
In [20]:
Level(a, [-3])
Out[20]:
$$ \left[ z1,\text{Cos} \! \left( x1 + y1 \right) ,z,\text{Sin} \! \left( x + y \right) \right] $$
In [21]:
Level(a, [-4])
Out[21]:
$$ \left[ z1 \ \text{Cos} \! \left( x1 + y1 \right) ,z \ \text{Sin} \! \left( x + y \right) \right] $$

Notice that negative levels generally can not be reduced to positive levels - they are giving in general different types of information. What we have just described is called the Standard Level Specification in Symata. Many more built - in commands accept level specification as one of the arguments (often an optional one).

Any function can be used also in its literal equivalent form. For instance :

In [22]:
[Plus(1, 2, 3, 4), Times(1, 2, 3, 4)]
Out[22]:
$$ \left[ 10,24 \right] $$

Second principle: pattern-matching and rule substitution

Another fundamental principle is so - called pattern - matching, which is a system to match rules and expressions - without it Symata would not know when to apply which rule. It is based on syntactic rather than semantic comparison of expressions. The main notions here are those of rules and patterns.

Rewrite Rules

In [23]:
Clear(a, b, c, d, e)

A typical rule looks like this:

In [24]:
a => b
Out[24]:
$$ a \Rightarrow b $$

where in general <a> and <b> are some expressions. The rule just says: whenever <a> is encountered, replace it by <b>. For example:

In [25]:
[a, c, d, c] ./ (a => b)
Out[25]:
$$ \left[ b,c,d,c \right] $$

(the ./ symbol is a rule replacement command, to be covered later).

A pattern is essentially any expression with some part of it replaced by "blank" (Blank()), which is a placeholder for any expression - that is, instead of that part there can be anything (this is somewhat oversim- plified). The literal equivalent for Blank[] is the single underscore ("_") symbol. For instance, f(x_) means f(anything).

An example of a simple pattern-defined function

In [26]:
Clear(f)
f(x_) := x^2
[f(2), f("word"), f(Newton)]
Out[26]:
$$ \left[ 4,\text{"word"}^{2},\text{Newton}^{2} \right] $$

In this example, the result is as shown because the definition of the function f is really just a substitution rule f(anything) -> (anything)^2.

To see the internal form of this rule - how it is stored in the rule base - one can use the built-in DownValues command. With its help we see:

In [27]:
DownValues(f)
Out[27]:
$$ \left[ \text{HoldPattern} \! \left( f \! \left( x\text{_} \right) \right) \text{:>}x^{2} \right] $$

We will talk later about the meaning of the HoldPattern function. The pattern x_ is the most simple pattern. There can be more complex patterns, both syntactically and also because patterns may have conditions attached to them, which ensure that the pattern will match only if the condition is satisfied (conditional patterns). We will cover them in detail later.

Example of a function based on a restricted pattern

Now let us give an example: we will restrict our function f to operate only on integers.

In [28]:
Clear(f)
f(x_Integer) := x^2
[f(2), f("word"), f(Newton)]
Out[28]:
$$ \left[ 4,f \! \left( \text{"word"} \right) ,f \! \left( \text{Newton} \right) \right] $$

In this case, we introduced a more complex pattern x_Integer.

A bit about evaluation

On this example we see that if there is no rule whose pattern (left hand side of the rule) matches a given expression, Symata returns the expression unchanged. This is at the heart of its evaluation method: to any entered expression, all rules which are in the global rule base at the moment of evaluation, are applied iteratively. Whenever some rule applies, an expression is rewritten and the process starts over. At some point, the expression becomes such that no rule can be applied to it, and this expression is the result. Since the rule base contains both system and user-defined rules (with the latter having higher priority), it gives great flexibility in manipulation of expressions.

Patterns allow for multiple definitions of the same function

As another starting example, let us define a function which is linear on even numbers, quadratic on odd numbers and is a Sin function for all other inputs:

In [29]:
Clear(f)
f(x_`EvenQ`) := x
f(x_`OddQ`) := x^2
f(x_) := Sin(x)
In [30]:
[f(1), f(2), f(3), f(4), f(3 / 2), f(Newton), f(Pi)]
Out[30]:
$$ \left[ 1,2,9,4,\text{Sin} \! \left( \frac{3}{2} \right) ,\text{Sin} \! \left( \text{Newton} \right) ,0 \right] $$

For the record, built-in functions OddQ and EvenQ are predicates which return True if the number is odd (even) and False otherwise :

In [31]:
[EvenQ(2), EvenQ(3), OddQ(2), OddQ(3)]
Out[31]:
$$ \left[ \text{True},\text{False},\text{False},\text{True} \right] $$

If nothing is known about the object, they give False :

In [32]:
[EvenQ(Newton), OddQ(Newton)]
Out[32]:
$$ \left[ \text{False},\text{False} \right] $$

Automatic rule reordering

Automatic rule reordering is not yet implemented:

In [33]:
Clear(f)
f(x_) := Sin(x)
f(x_`EvenQ`) := x
f(x_`OddQ`) := x^2
[f(1), f(2), f(3), f(4), f(3 / 2), f(Newton), f(Pi)]
Out[33]:
$$ \left[ \text{Sin} \! \left( 1 \right) ,\text{Sin} \! \left( 2 \right) ,\text{Sin} \! \left( 3 \right) ,\text{Sin} \! \left( 4 \right) ,\text{Sin} \! \left( \frac{3}{2} \right) ,\text{Sin} \! \left( \text{Newton} \right) ,0 \right] $$

To see the order in which the rules are kept, we again use DownValues :

In [34]:
DownValues(f)
Out[34]:
$$ \left[ \text{HoldPattern} \! \left( f \! \left( x\text{_} \right) \right) \text{:>}\text{Sin} \! \left( x \right) ,\text{HoldPattern} \! \left( f \! \left( \text{PatternTest} \! \left( x\text{_},\text{EvenQ} \right) \right) \right) \text{:>}x,\text{HoldPattern} \! \left( f \! \left( \text{PatternTest} \! \left( x\text{_},\text{OddQ} \right) \right) \right) \text{:>}x^{2} \right] $$

Third principle: expression evaluation

The last example brings us to the third principle: the principle of expression evaluation and the rewrite rules (global rule base). It tells the following: when Symata encounters an arbitrary expression, it checks its global base of rewrite rules for rule(s) which correspond to a given expression (or, it is said, match the expression). A typical rewrite rule looks like object1 -> object2. If such a rule is found, for expression or any of the subexpressions (actually, normally in reverse order), the (sub) expression is rewritten, and the process starts over. This process goes on until no further rule in the global rule base is found which matches the expression or any of its parts. When the expression stops changing, it is returned as the answer. Please bear in mind that the picture just described is a great oversimplification, and the real evaluation process is much more subtle, although the main idea is this.

The global rule base contains both rules built in the kernel and rules defined by the user. User-defined rules usually take precedence over the system rules, which makes it possible to redefine the behavior of almost any built-in function if necessary. In fact, all assignments to all variables and all function defini- tions are stored as some type of global rules, in the rule base. In this sense, there is no fundamental differ- ence between functions and variables (although there are technical differences).

As a result of this behavior, we get for instance such a result:

In [35]:
FullForm(Sin(Pi + Pi))
Out[35]:
0

The reason is that inside the kernel there are rules like Plus[x,x]->2 x, Sin[2*Pi]->0, and because the evaluation process by default starts with the innermost sub-expressions (leaves), i.e., from inside out, it produces 0 before the FullForm has any chance to "look" at the expression. The internal evaluation dynamics can be monitored with the Trace command:

In [36]:
Trace(True)
FullForm(Sin(Pi + Pi))
2<< False
 >>2 Sin(Plus(Pi,Pi))
  >>3 Sin(Pi + Pi)
   >>4 Pi + Pi
   4<< 2Pi
   >>4 2Pi
   4<< 2Pi
  3<< 0
 2<< 0
 >>2 0
 2<< 0
1<< 0
>>1 0
1<< 0
Out[36]:
0
In [37]:
Trace(False);
>>1 CompoundExpression(Trace(False))
 >>2 Trace(False)

Summary

To summarize, we have described briefly the main principles of Mathematica and along the way gave examples of use of the following built-in functions: AtomQ, Head, FullForm, Level, Plus, Times, Trace, DownValues, OddQ, EvenQ.

In [38]:
VersionInfo()
Symata version     0.4.6
Julia version      1.6.0-DEV.116
Python version     3.8.3
SymPy version      1.5.1
In [39]:
InputForm(Now())
Out[39]:
2020-05-31T10:22:55