Introduction to Symata

Load Symata like this


In [1]:
using Symata;

Symata is a computer language written in Julia. After typing using Symata, you are in Symata mode and input is interpreted as Symata language expressions.

Here is a Symata expression


In [2]:
(x+y)^3


Out[2]:
$$ \left( x + y \right) ^{3} $$

In a Jupyter notebook, you can exit Symata mode and enter Julia mode by entering Julia(). Expressions are then Julia language expressions.


In [3]:
Julia()


Out[3]:
true

In [4]:
length(zeros(10)) ==  10   # A Julia expression


Out[4]:
true

In Jupyter, type isymata() to leave Julia mode and enter Symata mode. At the command line REPL, type = at the beginning of a line to enter Symata mode.


In [5]:
isymata()   # we enter Symata mode again

Note: to leave Symata mode and return to Julia mode, type Julia() in IJulia, or backspace at the command line REPL.

Now we can enter Symata expressions.

Entering expressions

You enter expressions and Symata evaluates them


In [6]:
Cos(π * x)


Out[6]:
$$ \text{Cos} \! \left( \pi \ x \right) $$

A variable is set like this.


In [7]:
x = 1/3


Out[7]:
$$ \frac{1}{3} $$

Now the cosine is evaluated


In [8]:
Out(6)


Out[8]:
$$ \frac{1}{2} $$

In [9]:
x = 1/6


Out[9]:
$$ \frac{1}{6} $$

In [10]:
Out(6)


Out[10]:
$$ \frac{3^{\frac{1}{2}}}{2} $$

Clear the value of x and the cosine can no longer be reduced.


In [11]:
Clear(x)

Out(6)


Out[11]:
$$ \text{Cos} \! \left( \pi \ x \right) $$

It is clear what happened above. If x is not set to a value, then Cos(π * x) can't be written in a simpler form. If we set x to some particular values, then Cos(π * x) can be reduced to a simpler form.

(you can skip the following the first time through)

  • But, the reason Symata understands this is a consquence of the procedure it follows in evaluating (almost) all expressions. Symata evaluates expressions to a fixed point. More precisely, when an expression is given as input, Symata descends depth-first evaluating each subexpression to a fixed point and finally the top-level expression to a fixed point. When Cos(π * x) is first evaluated, each of π and x evaluates to itself so that π * x is already at a fixed point. Since there is no rule for evaluating Cos(π * x) for fixed π * x, Cos(π * x) is also at a fixed point.

  • The expression x=1/3 means that, whenever x is encountered, it should evaluate to 1/3. The expression Out(4) evaluates to the fourth output cell, which is Cos(π * x). Then π evaluates to iteself, x evaluates to 1, so that π * x evaluates to π/3. There is a rule saying that Cos(x/3) evaluates to 1/2.

  • Clear(x) says that x should once again evaluate to itself. Then evaluating Out(4) follows the same evaluation sequence, leading to Cos(π * x)

Assigning values

There are several kinds of assignment in Symata. The two most common are = (or Set) and := (or SetDelayed). Set immediatley evaluates the right hand side and binds the left hand side to the result. SetDelayed does not evaluate the right hand side when the assignment is made. It evaluates the right hand side every time the left hand side is subsequently evalutated and then binds the result.

The following demonstrates the difference.


In [12]:
x = 1
a := x
b = x
c = a
d := a

In [13]:
[x,a,b,c,d]


Out[13]:
$$ \left[ 1,1,1,1,1 \right] $$

In [14]:
ClearAll(x)

In [15]:
[x,a,b,c,d]


Out[15]:
$$ \left[ x,x,1,1,x \right] $$

In [16]:
(a = z, [x,a,b,c,d])


Out[16]:
$$ \left[ x,z,1,1,z \right] $$

In [17]:
ClearAll(x,a,b,c,d)

Destructuring assignment

Assign two variables at once


In [18]:
[a,b] = [x,y]


Out[18]:
$$ \left[ x,y \right] $$

In [19]:
a


Out[19]:
$$ x $$

In [20]:
b


Out[20]:
$$ y $$

In [21]:
[a,b]


Out[21]:
$$ \left[ x,y \right] $$

Swap two values


In [22]:
[a,b] = [b,a]


Out[22]:
$$ \left[ y,x \right] $$

In [23]:
[a,b]


Out[23]:
$$ \left[ y,x \right] $$

Expressions and parts of expressions

Expression is the central concept in Symata. In general, expressions are trees whose branches and leaves are other expressions. You can manipulate these expressions.

Every expression has a Head. For function-like expressions, the Head is the function name. For atomic expressions, the Head usually is a data type.


In [24]:
Map(Head, [x, x + y, [x,y], Cos(x), f(x), 3, 3.0, BI(3), BF(3)])  # apply the fun


Out[24]:
$$ \left[ \text{Symbol},\text{Plus},\text{List},\text{Cos},f,\text{Int64},\text{Float64},\text{BigInt},\text{BigFloat} \right] $$

In [25]:
expr = Expand((x+y)^3)


Out[25]:
$$ x^{3} + 3 \ x^{2} \ y + 3 \ x \ y^{2} + y^{3} $$

In [26]:
FullForm(expr)   # This shows the internal form. The tree is explicit


Out[26]:
Plus(Power(x,3),Times(3,Power(x,2),y),Times(3,x,Power(y,2)),Power(y,3))

In [27]:
expr[2,2,1]   # Return a part of the expression by index into the tree


Out[27]:
$$ x $$

In [28]:
expr[2,2,1] = z;  # Replace a part of the expression


Out[28]:
$$ z $$

In [29]:
expr


Out[29]:
$$ x^{3} + 3 \ z^{2} \ y + 3 \ x \ y^{2} + y^{3} $$

In [30]:
Part(expr,2,2,1)  # You can do the same thing with Part


Out[30]:
$$ z $$

In [31]:
Expand((x+y)^3)[4,1]   # You can get parts of expressions directly


Out[31]:
$$ y $$

In [32]:
expr = Expand((x+y)^20);

In [33]:
expr[14:18:2]  # Parts 14 through 18 with step 2


Out[33]:
$$ 77520 \ x^{7} \ y^{13} + 15504 \ x^{5} \ y^{15} + 1140 \ x^{3} \ y^{17} $$

In [34]:
ClearAll(expr)

Define a function that collects an expression's head and arguments in a list.


In [35]:
headargs(f_(args__)) := [f,args]

In [36]:
headargs(a + b^2 + 3)


Out[36]:
$$ \left[ \text{Plus},3,x^{2},y \right] $$

In [37]:
Integrate(f(x),x)


Out[37]:
$$ \int f \! \left( x \right) \, \mathbb{d}x $$

In [38]:
headargs(Integrate(f(x),x))


Out[38]:
$$ \left[ \text{Integrate},f \! \left( x \right) , \left[ x \right] \right] $$

In [39]:
ClearAll(a,b,c,d)

Rotate the head and arguments to make a new expression.


In [40]:
rotheadargs(f_(args__)) := (Last([args])(f,Splat(Most([args]))))

In [41]:
rotheadargs( a + b + c + d)


Out[41]:
$$ d \! \left( \text{Plus},a,b,c \right) $$

In [42]:
rotheadargs( a + b + c + d + g(x))


Out[42]:
$$ g \! \left( x \right) \! \left( \text{Plus},a,b,c,d \right) $$

Definition

Definition shows the definitions associated with a symbol


In [43]:
ClearAll(x,a,b,c,d)  # delete definitions from the previous example

In [44]:
a = 1


Out[44]:
$$ 1 $$

In [45]:
Definition(a)


a=1



In [46]:
a := x

Definition(a)  # This overwrites the previous definition


a:=x



In [47]:
f(x_) := x^2
f(x_, y_) := x + y
Definition(f)


f(x_):=x^2
f(x_,y_):=(x + y)



In [48]:
Definition(f)


f(x_):=x^2
f(x_,y_):=(x + y)



In [49]:
ClearAll(f,a)

Cpu time, memory, and tracing the evaluation


In [50]:
Timing((Range(10^6), Null ) )   # time a single expression


Out[50]:
$$ \left[ 0.331790975,\text{Null} \right] $$

Time toggles the timing of every expression entered. Memory allocated and the number of attempts to apply a user defined rule are also printed.


In [51]:
Time(True)  #  toggle timing all expressions. returns the previous value


Out[51]:
$$ \text{False} $$

In [52]:
Range(10^6);


  0.019286 seconds (999.57 k allocations: 22.887 MB, 34.43% gc time)
tryrule count: downvalue 0, upvalue 0

In [53]:
Time(False);


  0.000052 seconds (35 allocations: 3.109 KB)
tryrule count: downvalue 0, upvalue 0

In [54]:
Trace(True);  # Trace evaluation


2<< False
1<< False
Out[54]:
$$ \text{False} $$

In [55]:
(a+b)*(a+b)


>>1 CompoundExpression(nothing,(a + b)*(a + b))
 >>2 (a + b)*(a + b)
  >>3 a + b
  3<< a + b
  >>3 a + b
  3<< a + b
 2<< (a + b)^(1 + 1)
 >>2 (a + b)^(1 + 1)
  >>3 1 + 1
  3<< 2
 2<< (a + b)^2
 >>2 (a + b)^2
 2<< (a + b)^2
1<< (a + b)^2
>>1 (a + b)^2
1<< (a + b)^2
Out[55]:
$$ \left( a + b \right) ^{2} $$

In [56]:
Trace(False);


>>1 CompoundExpression(nothing,Trace(False))
 >>2 Trace(False)

In [57]:
? LeafCount


LeafCount(expr)

gives the number of indivisible (Part can't be taken) elements in expr.

This amounts to counting all the Heads and all of the arguments that are not of type Mxpr, that is compound expressions.

A more accurate name is NodeCount.

Help( LeafCount)


 Attributes(LeafCount) = [Protected]

In [58]:
LeafCount(Expand((a+b)^3))


Out[58]:
$$ 19 $$

In [59]:
ByteCount(Expand((a+b)^3))


Out[59]:
$$ 446 $$

In [60]:
? Depth


Depth(expr)

gives the maximum number of indices required to specify any part of expr, plus 1.

Help( Depth)


 Attributes(Depth) = [Protected]

In [61]:
Depth(Expand((a+b)^3))


Out[61]:
$$ 4 $$

In [62]:
FullForm(Expand((a+b)^3))  # Examine the tree


Out[62]:
Plus(Power(a,3),Times(3,Power(a,2),b),Times(3,a,Power(b,2)),Power(b,3))

In [63]:
Expand((a+b)^3)[2,2,1]    # One of the deepest parts


Out[63]:
$$ a $$

Calculations using expressions or functions

Here are a few ways to compute an integral


In [64]:
Integrate( (1+x^2)^(-1), x)


Out[64]:
$$ \text{ArcTan} \! \left( x \right) $$

In [65]:
expr = 1/(1+x^2)
Integrate(expr, x)


Out[65]:
$$ \text{ArcTan} \! \left( x \right) $$

In [66]:
f(x_) := 1/(1+x^2)
Integrate(expr, x)


Out[66]:
$$ \text{ArcTan} \! \left( x \right) $$

In [67]:
g(x_) = expr # Note we do not use ":="


Out[67]:
$$ \left( 1 + x^{2} \right) ^{-1} $$

In [68]:
ClearAll(expr)  # We did not use SetDelay, so we can delete expr

In [69]:
Integrate(g(y),y)


Out[69]:
$$ \text{ArcTan} \! \left( y \right) $$

Note: Trying to use a compiled (Julia) function h = :( x -> 1/(1+x^2)) will not work.


In [70]:
ClearAll(f,g,expr)

Integrate(f(y), y)  # The integral can no longer be reduced


Out[70]:
$$ \int f \! \left( y \right) \, \mathbb{d}y $$

Patterns and Matching

A Blank with no constraints matches everything


In [71]:
MatchQ(z,_)


Out[71]:
$$ \text{True} $$

In [72]:
Map(MatchQ(_), [1,"string", a+b, 1/3])  # MatchQ does Currying with the first argument


Out[72]:
$$ \left[ \text{True},\text{True},\text{True},\text{True} \right] $$

_head is a Blank that only matches expressions with Head equal to head.


In [73]:
FullForm(_Integer)  # underscore is shorthand for Blank


Out[73]:
Blank(Integer)

In [74]:
MatchQ(1, _Integer)


Out[74]:
$$ \text{True} $$

Use Currying to define a predicate function


In [75]:
myintq = MatchQ(_Integer);

Not all rational numbers are integers


In [76]:
Map(myintq, Range(1/2,5,1/2))


Out[76]:
$$ \left[ \text{False},\text{True},\text{False},\text{True},\text{False},\text{True},\text{False},\text{True},\text{False},\text{True} \right] $$

In [77]:
MatchQ(b^2, _^2)  # Match power with exponent equal to 2


Out[77]:
$$ \text{True} $$

In [78]:
MatchQ(b^3, _^_)   # Match any power


Out[78]:
$$ \text{True} $$

In [79]:
MatchQ((b+c)^3, _^_)


Out[79]:
$$ \text{True} $$

In [80]:
MatchQ(b^1, _^_)


Out[80]:
$$ \text{False} $$

This failed because b^1 evaluates to b, which does not have the structure of a power

The pattern can be complex with blanks deep in an expression.


In [81]:
Map(MatchQ(f(x_^2)), [f(b^2), f(b^3), g(b^2)])


Out[81]:
$$ \left[ \text{True},\text{False},\text{False} \right] $$

Specify a "function" Head that must match


In [82]:
Map( MatchQ(_gg), [gg(x+y), gg(x), g(x)])


Out[82]:
$$ \left[ \text{True},\text{True},\text{False} \right] $$

Define a predicate for positive integers


In [83]:
m = MatchQ(_Integer:?(Positive))

Map(m, [1,100, 0, -1, 1.0, x])


Out[83]:
$$ \left[ \text{True},\text{True},\text{False},\text{False},\text{False},\text{False} \right] $$

We can also put a condition on a Pattern. This matches pairs with the first element smaller than the second.


In [84]:
m = MatchQ(Condition([x_, y_], x < y))

[ m([2,1]), m([1,2]), m([1,2,3]), m(1)]


Out[84]:
$$ \left[ \text{False},\text{True},\text{False},\text{False} \right] $$

Patterns can include Alternatives.


In [85]:
m = MatchQ(_Integer | _String)

[m(1), m("zebra"), m(1.0)]


Out[85]:
$$ \left[ \text{True},\text{True},\text{False} \right] $$

Repeated(expr) matches one or more occurences of expr.


In [86]:
MatchQ([a,a,a,b], [Repeated(a),b])


Out[86]:
$$ \text{True} $$

RepeatedNull matches zero or more occurences.


In [87]:
MatchQ([b], [RepeatedNull(a),b])


Out[87]:
$$ \text{True} $$

In [88]:
ClearAll(m)

Replacing

Rules are used for many things in Symata, including replacement. Replacement is a key ingredient in the implementation of functions.

When applied, this rule matches and does a replacement on any expression with Head f and a List of two elements as the sole argument.


In [89]:
f([x_,y_]) => p(x+y)


Out[89]:
$$ f \! \left( \left[ x\text{_},y\text{_} \right] \right) \Rightarrow p \! \left( x + y \right) $$

In [90]:
expr = f([x+y,y]) + f(c) + g([a,b])


Out[90]:
$$ f \! \left( c \right) + f \! \left( \left[ x + y,y \right] \right) + g \! \left( \left[ a,b \right] \right) $$

In [91]:
ReplaceAll( expr, f([x_,y_]) => p(x+y))


Out[91]:
$$ f \! \left( c \right) + g \! \left( \left[ a,b \right] \right) + p \! \left( x + 2 \ y \right) $$

There are several things to note here.

  • The pattern x_ puts no restrictions on the match; any expression will match. The name of the pattern x only serves to identify it later during a replacement.

  • Here x_ has matched x+y, but these two uses of x are not confused in the result. That is, in x_, the symbol x is a dummy variable.

  • The expression f(c) has a matching Head, but not matching arguments, so f(c) fails to match. Likewise, the expression g([a,b]) has matching arguments, but not matching head.

  • The expression f([x+y,y]) matches, and the replacement is made in (a copy of) expr. But, Symata alays evaluates expressions to a fixed point. So y+y is replaced by 2y, and the terms in expr are rearranged into the canonical order.

Again, we have to be aware that matching is structural.


In [92]:
ReplaceAll([a/b, 1/b^2, 2/b^2] , b^n_ => d(n))


Out[92]:
$$ \left[ a \ d \! \left( -1 \right) ,d \! \left( -2 \right) ,2 \ d \! \left( -2 \right) \right] $$

In [93]:
ClearAll(expr)

Named patterns that appear in more than one place must match the same expression.


In [94]:
ReplaceAll( [b,a,[a,b]] , [x_,y_,[x_,y_]] => 1 )  # This does not match


Out[94]:
$$ \left[ b,a, \left[ a,b \right] \right] $$

In [95]:
ReplaceAll( [a,b,[a,b]] , [x_,y_,[x_,y_]] => 1 )  # This does match


Out[95]:
$$ 1 $$

This example uses Alternatives.


In [96]:
ReplaceAll( [a, b, c, d, a, b, b, b],  a | b => x)


Out[96]:
$$ \left[ x,x,c,d,x,x,x,x \right] $$

The arguments of Sequence are spliced into expressions during evaluation.


In [97]:
[1,2,Sequence(a,b)]


Out[97]:
$$ \left[ 1,2,a,b \right] $$

An unmatched alternative is replaced by Sequence(). Upon evaluation to fixed point, this empty sequence is removed.


In [98]:
f(x_, x_ | y_String) := [x,y]

In [99]:
f(2,2)  # `y` does not match, so it is removed.


Out[99]:
$$ \left[ 2 \right] $$

In [100]:
f(2,"cat")  # Here the second Alternative matches


Out[100]:
$$ \left[ 2,\text{"cat"} \right] $$

In [101]:
f(2,3)      # Here the Pattern fails to match.


Out[101]:
$$ f \! \left( 2,3 \right) $$

Alternatives, and Patterns in general, can be explicit expressions, with no Blanks.


In [102]:
( h(a | b) := p, [h(a), h(b), h(c), h(d)] )


Out[102]:
$$ \left[ p,p,h \! \left( c \right) ,h \! \left( d \right) \right] $$

ReplaceAll replaces all matching subexpressions. We can also specify the levels. This matches at level 2 and deeper.


In [103]:
Replace(1 + a + f(a) + g(f(a)), a => b, 2)


Out[103]:
$$ 1 + b + f \! \left( b \right) + g \! \left( f \! \left( a \right) \right) $$

This replaces only at level 2.


In [104]:
Replace(1 + a + f(a) + g(f(a)), a => b, [2]) == 1 + a + f(b) + g(f(a))


Out[104]:
$$ \text{True} $$

Rule evaluates the right hand side once, when it is first evaluated.


In [105]:
ReplaceAll( [x,x,x,x,x],  x  => RandomReal() )


Out[105]:
$$ \left[ 0.030159732880878254,0.030159732880878254,0.030159732880878254,0.030159732880878254,0.030159732880878254 \right] $$

RuleDelayed evaluates the right hand side every time it is applie


In [106]:
ReplaceAll( [x,x,x,x,x],  RuleDelayed(x ,RandomReal()))


Out[106]:
$$ \left[ 0.26230928906640605,0.09273232724891844,0.59171253883417,0.8584123029537587,0.03374763999914698 \right] $$

Except matches everything except expressions that match its argument. The following applies the replacement at level 1.


In [107]:
Replace([1, 7, "Hi", 3, Indeterminate], Except(_:?(NumericQ)) => 0, 1)


Out[107]:
$$ \left[ 1,7,0,3,0 \right] $$

Each Rule in a List of Rules is tried in turn. Matching stops after the first match. ReplaceRepeated continues applying Rules until the expression reaches a fixed point.


In [108]:
ReplaceRepeated(x^2 + y^6 , List(x => 2 + a, a => 3))


Out[108]:
$$ 25 + y^{6} $$

Up to this point, we have used named patterns only with a single blank, for example b_. But, we may associate aname with any pattern expression, including a complex (compound) expression.


In [109]:
ReplaceAll( b^c, a::(_^_) => g(a))


Out[109]:
$$ g \! \left( b^{c} \right) $$

Patterns are used to implement optional arguments.


In [110]:
f(x_, y_:3) := x + y

[f(a+b,z), f(a+b)]


Out[110]:
$$ \left[ a + b + z,3 + a + b \right] $$

Condition may be used in definitions like this:


In [111]:
ClearAll(f)

f(x_) :=  Condition(x^2, x > 3)

In [112]:
[f(4),f(3)]


Out[112]:
$$ \left[ 16,f \! \left( 3 \right) \right] $$

In [ ]:

We can match and replace with a Pattern with Head equal to Plus


In [113]:
ReplaceAll( z*y + b , x_ + y_ => x * y )


Out[113]:
$$ b \ y \ z $$

In [114]:
ReplaceAll( z*y + b +c , x_ + y_ => x * y)


Out[114]:
$$ b + c + y \ z $$

This failed because Plus with two terms does not match Plus with three terms. But, we actually do want this to match. Implementing associative-commuatative matching is a major goal of Symata. Anyone want to give it a try ?


In [115]:
ClearAll(f,h,a,b,x,y)

Interface to compiled languages

Symata's host language is Julia, a high-performance, compiled language. It can be useful to call Juila code from Symata or to compile Symata code to Julia. Symata is also an open-source project, which means you can alter or add to it directly.

Define a compiled function to a built-in or user-defined Julia function like this

Calling compiled functions


In [116]:
mylog = J(log )

mylog(2,2)


Out[116]:
$$ 1.0 $$

You can also easily write compiled code like this


In [117]:
f = J( (x,y)  ->  x^2 + y^2 )

f(3.0,4.0)


Out[117]:
$$ 25.0 $$

Note that we did not specify the data types. Is this really high-performance compiled code ? Yes it is. The function was compiled after we called it with two floating point numbers. If we call the function with two integers, a version (called a method) that is optimized for integers is compiled. A version optimized for an integer and a rational number or any combination of arguments can also be compiled.


In [118]:
[f(3,4), f(3, 1/2)]


Out[118]:
$$ \left[ 25,\frac{37}{4} \right] $$

We define two versions of the same function to see the difference in performance between compiled functions and functions defined via Rules.


In [119]:
(a = Range(0.0,100.,.01), ccossq = J( x -> cos(x)^2 ), cossq(x_) := cos(x)^2);

In [120]:
Time(True);

We run each test twice because compilation time is included in the first run.


In [121]:
Map(cossq, a);


  1.921082 seconds (2.88 M allocations: 174.656 MB, 8.01% gc time)
tryrule count: downvalue 10001, upvalue 0

In [122]:
Map(cossq, a);


  1.194949 seconds (2.59 M allocations: 162.150 MB, 7.09% gc time)
tryrule count: downvalue 10001, upvalue 0

In [123]:
Map(ccossq, a);


  0.018044 seconds (43.26 k allocations: 1014.076 KB)
tryrule count: downvalue 0, upvalue 0

In [124]:
Map(ccossq, a);


  0.002035 seconds (40.11 k allocations: 867.703 KB)
tryrule count: downvalue 0, upvalue 0

In [125]:
(Time(False), ClearAll(f,a,ccossq,cossq,mylog))


  0.000325 seconds (3.06 k allocations: 103.266 KB)
tryrule count: downvalue 0, upvalue 0

The compiled function is about $500$ times faster in this example. Symata Patterns can be compiled (automatically) as well, but this has been removed during a rewriting of the Pattern code that is still underway. With compiled Patterns, the factor might be closer to $50$.

Compiling Symata expressions

In this example we calculate an expression and compile it. The compiled code is as efficient hand-coded Julia.


In [126]:
expr = Integrate( x^2 * Exp(x)* Cos(x), x)


Out[126]:
$$ \frac{- \ \mathbb{e} ^{x} \ \text{Cos} \! \left( x \right) }{2} + \frac{ \mathbb{e} ^{x} \ x^{2} \ \text{Cos} \! \left( x \right) }{2} + \frac{ \mathbb{e} ^{x} \ \text{Sin} \! \left( x \right) }{2} + \frac{ \mathbb{e} ^{x} \ x^{2} \ \text{Sin} \! \left( x \right) }{2} + - \ \mathbb{e} ^{x} \ x \ \text{Sin} \! \left( x \right) $$

In [127]:
expr = Collect(expr, Exp(x))


Out[127]:
$$ \mathbb{e} ^{x} \ \left( \frac{- \ \text{Cos} \! \left( x \right) }{2} + \frac{x^{2} \ \text{Cos} \! \left( x \right) }{2} + \frac{\text{Sin} \! \left( x \right) }{2} + \frac{x^{2} \ \text{Sin} \! \left( x \right) }{2} + - \ x \ \text{Sin} \! \left( x \right) \right) $$

In [128]:
cexpr = Compile(expr)
a = Range(0.0,10.0,.01);

In [129]:
Timing((Map(cexpr, a), Null))


Out[129]:
$$ \left[ 0.008887768,\text{Null} \right] $$

In [130]:
Timing((Map(cexpr, a), Null))


Out[130]:
$$ \left[ 0.000162092,\text{Null} \right] $$

In [131]:
ClearAll(a,expr,cexpr)