Skip to content

Commit 7e259fd

Browse files
committed
Update rewriters part of the manual
1 parent f643117 commit 7e259fd

File tree

2 files changed

+78
-22
lines changed

2 files changed

+78
-22
lines changed

page/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ where appropriate -->
1818
**Features:**
1919

2020
- Fast expressions
21-
- A [combinator library](/rewrite/#composing_rewriters) for making rewriters.
2221
- A [rule-based rewriting language](/rewrite/#rule-based_rewriting).
22+
- A [combinator library](/rewrite/#composing_rewriters) for making rewriters.
2323
- Type promotion:
2424
- Symbols (`Sym`s) carry type information. ([read more](#creating_symbolic_expressions))
2525
- Compound expressions composed of `Sym`s propagate type information. ([read more](#expression_interface))

page/rewrite.md

Lines changed: 77 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
Rewrite rules match and transform an expression. A rule is written using either the `@rule` macro or the `@acrule` macro. It creates callable `Rule` object.
66

7+
### Basics of rule-based term rewriting in SymbolicUtils
8+
79
Here is a simple rewrite rule, that uses formula for the double angle of the sine function:
810

911
```julia:rewrite1
@@ -44,7 +46,7 @@ Rules are of course not limited to single slot variable
4446
```julia:rewrite5
4547
r2 = @rule sin(~x + ~y) => sin(~x)*cos(~y) + cos(~x)*sin(~y);
4648
47-
r2(sin(a + b))
49+
r2(sin(α+β))
4850
```
4951

5052
If you want to match a variable number of subexpressions at once, you will need a **segment variable**. `~~xs` in the following example is a segment variable:
@@ -68,8 +70,6 @@ Notice that the expression was autosimplified before application of the rule.
6870
2 * (w+w+α+β)
6971
```
7072

71-
72-
7373
### Predicates for matching
7474

7575
Matcher pattern may contain slot variables with attached predicates, written as `~x::f` where `f` is a function that takes a matched expression and returns a boolean value. Such a slot will be considered a match only if `f` returns true.
@@ -102,7 +102,39 @@ acr = @acrule((~a)^(~x) * (~a)^(~y) => (~a)^(~x + ~y))
102102
acr(x^y * x^z)
103103
```
104104

105-
although in case of `Number` it also works with regular `@rule` since autosimplification orders and applies associativity and commutativity to the expression.
105+
although in case of `Number` it also works the same way with regular `@rule` since autosimplification orders and applies associativity and commutativity to the expression.
106+
107+
### Example of applying the rules to simplify expression
108+
109+
Consider expression `(cos(x) + sin(x))^2` that we would like simplify by applying some trigonometric rules. First, we need rule to expand square of `cos(x) + sin(x)`. First we try the simplest rule to expand square of the sum and try it on simple expression
110+
```julia:rewrite9
111+
using SymbolicUtils
112+
113+
@syms x::Real y::Real
114+
115+
sqexpand = @rule (~x + ~y)^2 => (~x)^2 + (~y)^2 + 2 * ~x * ~y
116+
117+
sqexpand((cos(x) + sin(x))^2)
118+
```
119+
120+
It works. This can be further simplified using Pythagorean identity and check it
121+
122+
```julia:rewrite10
123+
pyid = @rule sin(~x)^2 + cos(~x)^2 => 1
124+
125+
pyid(cos(x)^2 + sin(x)^2) === nothing
126+
```
127+
128+
Why does it return `nothing`? If we look at the rule, we see that the order of `sin(x)` and `cos(x)` is different. Therefore, in order to work, the rule needs to be associative-commutative.
129+
130+
```julia:rewrite11
131+
acpyid = @acrule sin(~x)^2 + cos(~x)^2 => 1
132+
133+
acpyid(cos(x)^2 + sin(x)^2 + 2cos(x)*sin(x))
134+
```
135+
136+
It has been some work. Fortunately rules may be [chained together](#chaining rewriters) into more sophisticated rewirters to avoid manual application of the rules.
137+
106138

107139
## Composing rewriters
108140

@@ -122,39 +154,63 @@ rewriters.
122154
- `IfElse(cond, rw1, rw2)` runs the `cond` function on the input, applies `rw1` if cond
123155
returns true, `rw2` if it retuns false
124156
- `If(cond, rw)` is the same as `IfElse(cond, rw, Empty())`
125-
- `Prewalk(rw; threaded=false, thread_cutoff=100)` returns a rewriter which does a pre-order
126-
traversal of a given expression and applies the rewriter `rw`. `threaded=true` will
127-
use multi threading for traversal. `thread_cutoff` is the minimum number of nodes
128-
in a subtree which should be walked in a threaded spawn.
129-
- `Postwalk(rw; threaded=false, thread_cutoff=100)` similarly does post-order traversal.
157+
- `Prewalk(rw; threaded=false, thread_cutoff=100)` returns a rewriter which does a pre-order
158+
(*from top to bottom and from left to right*) traversal of a given expression and applies
159+
the rewriter `rw`. `threaded=true` will use multi threading for traversal. `thread_cutoff`
160+
is the minimum number of nodes in a subtree which should be walked in a threaded spawn.
161+
- `Postwalk(rw; threaded=false, thread_cutoff=100)` similarly does post-order
162+
(*from left to right and from bottom to top*) traversal.
130163
- `Fixpoint(rw)` returns a rewriter which applies `rw` repeatedly until there are no changes to be made.
131164
- `PassThrough(rw)` returns a rewriter which if `rw(x)` returns `nothing` will instead
132165
return `x` otherwise will return `rw(x)`.
133166

167+
### Chaining rewriters
134168

135-
Example using Postwalk, and Chain
169+
Several rules may be chained to give chain of rules. Chain is an array of rules which are subsequently applied to the expression.
136170

137-
```julia:rewrite9
171+
To check that, we will combine rules from [previous example](#example of applying the rules to simplify expression) into a chain
138172

173+
```julia:composing1
139174
using SymbolicUtils
140175
using SymbolicUtils.Rewriters
141176
142-
r1 = @rule ~x + ~x => 2 * (~x)
143-
r2 = @rule ~x * +(~~ys) => sum(map(y-> ~x * y, ~~ys));
177+
sqexpand = @rule (~x + ~y)^2 => (~x)^2 + (~y)^2 + 2 * ~x * ~y
178+
acpyid = @acrule sin(~x)^2 + cos(~x)^2 => 1
144179
145-
rset = Postwalk(Chain([r1, r2]))
146-
rset_result = rset(2 * (w+w+α+β))
180+
csa = Chain([sqexpand, acpyid])
147181
148-
rset_result
182+
csa((cos(x) + sin(x))^2)
149183
```
150184

151-
It applied `r1`, but didn't get the opportunity to apply `r2`. So we need to apply the ruleset again on the result.
185+
Important feature of `Chain` is that it returns the expressiona instead of `nothing` if it doesn't change the expression
152186

153-
```julia:rewrite10
154-
rset(rset_result)
187+
```julia:composing2
188+
Chain([@acrule sin(~x)^2 + cos(~x)^2 => 1])((cos(x) + sin(x))^2)
189+
```
190+
191+
its important to notice, that chain is ordered, so if rules are in different order it wouldn't work the same as in earlier example
192+
193+
```julia:composing3
194+
cas = Chain([acpyid, sqexpand])
195+
196+
cas((cos(x) + sin(x))^2)
155197
```
198+
since Pythagorean identity is applied before square expansion, so it is unable to match squares of sine and cosine.
199+
200+
One way to circumvent the problem of order of applying rules in chain is to use `RestartedChain`
201+
202+
```julia:composing4
203+
using SymbolicUtils.Rewriters: RestartedChain
204+
205+
rcas = RestartedChain([acpyid, sqexpand])
206+
207+
rcas((cos(x) + sin(x))^2)
208+
```
209+
210+
It restarts the chain after each successful application of the rule, so after `sqexpand` is hit it (re)starts again and successfully applies `acpyid` to resulting expression.
156211

157212
You can also use `Fixpoint` to apply the rules until there are no changes.
158-
```julia:rewrite11
159-
Fixpoint(rset)(2 * (w+w+α+β))
213+
214+
```julia:composing5
215+
Fixpoint(cas)((cos(x) + sin(x))^2)
160216
```

0 commit comments

Comments
 (0)