All the examples can be run in stimsym_cli
or in the notebook.
Stimsym is a symbolic language, where we manipulate expressions, not
simply values. a+b
is just an expression that evaluates to itself, since
we do not know a concrete value for a
or for b
.
Similarly, f[a,b]
is the application of the function f
to arguments a,b
.
Since f
is not defined, the application does not evaluate, and it’s fine!
> 1 + 2 3 > 1 + a + 2 3+a > f[a,b,c] f[a,b,c] > f[a,1+2+c] f[a,3+c]
We can define (partially) a function using :=
.
There is no static typing, and no need to defined a function on all its
possible arguments:
> f[x_,0] := x > f[a, 0] a > f[b] (* no rule matches *) f[b] > f[a, 42] f[a,42] > f[a,0+0] a
In this definition, you might have noticed how x
appears with
the _ on the left, but not on the right.
x_ is actually a blank pattern: it matches any expression (any cargument)
and binds the argument to the variable x
.
So, g[x_,y_] := 2 x + y defines g
as a function that takes any two
expressions and sums them after doubling the first one: g[10,3]
will be 23
.
More complicated patterns will only match some expressions
(see Patterns).
Expressions are built from the following ingredients:
- integers
-
1
,-2
,1234542452626246246225114787
(arbitrary precision) - rationals
-
1/2
,-5/10
, etc. - strings
-
"ab cd"
,"f[a]"
,"1+1"
- symbols
-
any string composed of alphanumeric objects
- builtins
-
many builtin functions, such as
Plus
. They often have a shortcut representation, e.g.Plus[a,b,c]
isa+b+c+
,Times[a,2]
isa 2
, etc. - applications
-
expr[expr,expr,…]
applies the first expression to a sequence of arguments.f[]
appliesf
to 0 arguments;f[a]
appliesf
toa
;f[a][b,c]
appliesf[a]
to argumentsb, c
.
The primary container is the list, denoted List[a,b,c]
or {a,b,c}
.
However, it is possible to store elements under any symbol that is
not defined:
> {{a},{b,1+1}} {{a},{b,2}} > SomeSymbol[a,b,c] SomeSymbol[a,b,c]
The primary operation for evaluating expressions is rewriting.
Every definition (pattern := expr
) defines a rule that rewrites
anything matching the pattern, to the expression.
Some expressions define "local" rewriting rules:
-
pattern → expr
evaluatesexpr
first, then defines the rule mapping anything matchingpattern
to the evaluated expression. This is typically only useful ifpattern
binds no variables (e.g. ifpattern
is a constant) -
pattern :> expr
maps anything matchingpattern
toexpr
. Here,expr
is only evaluated once the pattern matches. For instance, f[x_] :> x+2 will rewritef[1]
to3
,f[98]
to100
, etc.
The following operators rewrite expressions using local rules:
-
expr //. rules
(whererules
is either one rule, or a list of rules) rewriteexpr
with the rules until no rule applies anymore. For example,> g[f[f[f[a]]]] //. {f[x_]:> x, g[x_] :> h[x]} h[a] > f[f[f[a]]] //. {f[x_]:> g[x], g[x_] :> h[x]} h[h[h[a]]]
-
expr /. rules
(whererules
is either one rule, or a list of rules) rewriteexpr
with the rules, but each sub-expression is rewritten at most once:> f[f[f[a]]] /. {f[x_]:> g[x], g[x_] :> h[x]} g[g[g[a]]]
- blank pattern
-
_ matches anything. x_ matches anything and binds
x
to it. - blank non empty sequence
-
__ matches any non-empty sequence of arguments: f[a, y__] := {y} will trigger on any expression
f[a,…]
and matchy
with the…
.
> f[a,y__] := {y} > f[a,b,c,d] {b,c,d} > f[a] (* does not match *) f[a] > f[b,c,d] f[b,c,d]
- blank sequence
-
___ matches any sequence of arguments, including an empty one: f[a, y___] := {y} is very similar to f[a,y__] := {y} but will also reduce
f[a]
to{}
. - test pattern
-
p?f
is a pattern that matches any expressione
againstp
, but only iff[e]
reduces toTrue
. Typically, _?IntegerQ matches any integer, _?RationalQ any rational (or integer). - conditional pattern
-
A pattern
p /; expr
matches the same expressions asp
(wherep
is a pattern), but only ifexpr
evaluates toTrue
. The testexpr
is expected to reduce toTrue
orFalse
; otherwise the evaluation fails. This is more powerful, but more verbose, than a test pattern: _?IntegerQ can be expressed as x_ /; IntegerQ[x].More advanced example involving both a test and a conditional (because the condition
a+2==3
does not reduce to a boolean, we guard x_ with anIntegerQ
test):> {1,2,a,4} /. (x_?IntegerQ /; (x+2 == 3) :> success[x]) {success[1],2,a,4}
A funny example of rewriting is the following bubble sort (not efficient,
but compact).
It repeatedly rewrites the list l
using the rule
{x___,y_,z_,k___}/;(y>z) :> {x,z,y,k}, which finds two elements y
and z
in a list, with y>z
, and swaps them.
Note how x___ and k___ capture the other elements of the list.
> sort[l_] := l //. {x___,y_,z_,k___}/;(y>z) :> {x,z,y,k} > sort[{64,44,71,48,96,47,59,71,73,51,67,50,26,49,49}] {26,44,47,48,49,49,50,51,59,64,67,71,71,73,96}
Stimsym is certainly not a (usable) computer algebra system, but it provides a few builtin operators.
> a===a (* syntactic equality *) True > a===b False > 10>5==5<=7 (* chain of tests *) True > a==a (* does not reduce *) a==a > a==a<b a==a<b > a==a<b /. {a->5,b->10} True > 2^10 1024 > 6! (* factorial *) 120 > a&&b || !c (* bool expressions *) a&&b||!c
Some handy functions:
- FullForm
-
shows the unsugared expression, very convenient for understanding some quirks of the parser:
> FullForm[a&&b||!c] Or[And[a,b],Not[c]]
- Nest
-
> Nest[f,a,10] f[f[f[f[f[f[f[f[f[f[a]]]]]]]]]] > (f^10)[a] (* short for Nest *) f[f[f[f[f[f[f[f[f[f[a]]]]]]]]]]
- Hold
-
blocks evaluation of its arguments.
> Hold[1+1] Hold[1+1]
The special symbol Sequence
has the special property that
it "flattens" when it appears in a list of arguments (or a list):
> Sequence[a,b] Sequence[a,b] > f[a,Sequence[b,c],d] f[a,b,c,d] > {Sequence[1,2],Sequence[3,4],5} {1,2,3,4,5}
Stimsym emphasizes functional programming and pure expressions.
Instead of loops, it provides a powerful comprehension mechanism.
A comprehension expression has the form expr ::cond1, cond2, …
,
a bit similar to python’s expr for … if …
construct.
Conditions are evaluated left-to-right, and have one of the forms:
-
pattern <- expr, will match
pattern
againstexpr
and bind variables ofpattern
in the remaining (right-side) conditions -
pattern <<- expr, will match
pattern
against every expressions immediately beneathexpr
, and bind variables ofpattern
in the remaining (right-side) conditions.For example, f[x_] <<- {1,f[2],3,f[4]} will match f[x_] with each element of the list in a backtracking fashion. First, it will try to match against
1
, fail, thenf[2]
, succeed in binding x_ <- 2, evaluate the remaining conditions, then backtrack, fail against 3, succeed againstf[4]
, evaluate the remaining conditions with x_ <- 4, and terminate. -
expr
, where the expression will be evaluated with the current bindings, and evaluation continues only ifexpr
reduces toTrue
. This is used to add tests, a bit likebar
in python’sexpr for x in foo if bar
.
Note that matching a pattern against an expression can return several results.
For instance, x_+y_ <- a+b will yield x=a,y=b
and x=b,y=a
.
In a comprehension, both choices will be considered and returned,
like clauses in Prolog.
The following expression computes the cartesian product of two lists:
> Product[l1_,l2_] := {{x,y} :: x_<<- l1, y_ <<- l2} > Product[{1,2,3},{a,b,c}] {{1,a},{1,b},{1,c},{2,a},{2,b},{2,c},{3,a},{3,b},{3,c}}
A comprehension returns a Sequence
, so it flattens under any
other symbol (such as {}
).
Very similar to the comprehension, Let
is an interesting variation.
Its full form is Let[cond1,…,condn, expr]
where the conditions are
similar to those in Comprehensions, but it only returns the first
successful expr
and discards the other choices.
If there is no successful answer (corresponding to an empty comprehension)
then evaluation fails.
> Let[x_<-1, y_<-2, 2 x+y] 4 > Let[x_+y_ <- a+b, f[x,y]] f[a,b] > {f[x,y]:: x_+y_ <-a+b} (* contrast with that *) {f[a,b],f[b,a]} > Let[x_?IntegerQ <- a, x] (* no answer -> failure! *) evaluation failed: no match for `Let`
There is a way to denote simple anonymous functions using the postfix &
operator and #1
, #2
, … for arguments. #0
is the whole sequence
of elements.
> (#1 &)[a,b,c] a > (f[#1,{#0}]&)[a,b,c] f[a,{a,b,c}] > (Plus[#0]!&)[1,2,3] (* sum, then factorial *) 720
Evaluating Doc[f]
where f
is a builtin symbol will display the
corresponding documentation:
> Doc[Plus] ================================================== # Plus Addition operator. Associative, Commutative, with regular evaluation on numbers. neutral element:: 0 infix form:: `a + b + c + d` ==================================================
- enumerate the way to split a sum in one atom + 2 sub-sums
-
To do so, we match
a+b+c+d
with the patternx_+y+z
, wherey
andz
match non-empty sequences. Using the Comprehensions mechanism, we build a termf[x,{y},{z}]
for each result of this matching, and wrap the result in a list.> {f[x,{y},{z}] :: x_+y__+z__<-a+b+c+d} {f[a,{c,b},{d}], f[a,{d,b},{c}], f[a,{b},{d,c}], f[a,{d,c},{b}], f[a,{c},{d,b}], f[a,{d},{c,b}], f[b,{c,a},{d}], f[b,{d,a},{c}], f[b,{a},{d,c}], f[b,{d,c},{a}], f[b,{c},{d,a}], f[b,{d},{c,a}], f[c,{b,a},{d}], f[c,{d,a},{b}], f[c,{a},{d,b}], f[c,{d,b},{a}], f[c,{b},{d,a}], f[c,{d},{b,a}], f[d,{b,a},{c}], f[d,{c,a},{b}], f[d,{a},{c,b}], f[d,{c,b},{a}], f[d,{b},{c,a}], f[d,{c},{b,a}]}
- compute
3!!!
-
we compute
(fun x → x!)
(i.e.(#! &)
) composed 3 times with itself (^3
) and then applied to 3.> ((#!&)^3)[3] 2601218943565795100204903227081043611191521875016945785727541837850835631156947382240678577958130457082619920575892247259536641565162052015873791984587740832529105244690388811884123764341191951045505346658616243271940197113909845536727278537099345629855586719369774070003700430783758997420676784016967207846280629229032107161669867260548988445514257193985499448939594496064045132362140265986193073249369770477606067680670176491669403034819961881455625195592566918830825514942947596537274845624628824234526597789737740896466553992435928786212515967483220976029505696699927284670563747137533019248313587076125412683415860129447566011455420749589952563543068288634631084965650682771552996256790845235702552186222358130016700834523443236821935793184701956510729781804354173890560727428048583995919729021726612291298420516067579036232337699453964191475175567557695392233803056825308599977441675784352815913461340394604901269542028838347101363733824484506660093348484440711931292537694657354337375724772230181534032647177531984537341478674327048457983786618703257405938924215709695994630557521063203263493209220738320923356309923267504401701760572026010829288042335606643089888710297380797578013056049576342838683057190662205291174822510536697756603029574043387983471518552602805333866357139101046336419769097397432285994219837046979109956303389604675889865795711176566670039156748153115943980043625399399731203066490601325311304719028898491856203766669164468791125249193754425845895000311561682974304641142538074897281723375955380661719801404677935614793635266265683339509760000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
- union in comprehension
-
> f[Set[RangeSeq[i]]:: i_<<-Range[10]] f[Set[0], Set[0,1], Set[0,1,2], Set[0,1,2,3], Set[0,1,2,3,4], Set[0,1,2,3,4,5], Set[0,1,2,3,4,5,6], Set[0,1,2,3,4,5,6,7], Set[0,1,2,3,4,5,6,7,8], Set[0,1,2,3,4,5,6,7,8,9], Set[0,1,2,3,4,5,6,7,8,9,10]] > Union[Set[RangeSeq[i]]:: i_<<-Range[10]] Set[0,1,2,3,4,5,6,7,8,9,10]