While watching the Mathologer masterclass on power sums
I came across a challenge to evaluate the following sum
This can be easily evaluated via brute force in Julia to yield
function power_cal(n) a=big(0) for i=1:n a+=big(i)^10 end a end julia> power_cal(1000) 91409924241424243424241924242500
Note the I had to use big
to makes sure we are using BigInt
in the computation. Without that, we would be quickly running in into an overflow issue and we will get a very wrong number.
In the comment section of the video I found a very elegant solution to the above sum, expressed as
(1/11) * 1000^11 + (1/2) * 1000^10 + (5/6) * 1000^9 – 1000^7 + 1000^5-(1/2) * 1000^3 + (5/66) * 1000 = 91409924241424243424241924242500
If I try to plug this into the Julia, I get
julia> (1/11) * 1000^11 + (1/2) * 1000^10 + (5/6) * 1000^9 - 1000^7 + r1000^5-(1/2) * 1000^3 + (5/66) * 1000 -6.740310541071357e18
This negative answer is not surprising at all, because we obviously ran into an overflow. We can, of course, go through that expression and modify all instances of Int64
with BigInt
by wrapping it in the big
function. But that would be cumbersome to do by hand.
The power of Metaprogramming
In Julia, metaprogramming allows you to write code that creates code, the idea here to manipulate the abstract syntax tree (AST) of a Julia expression. We start to by “quoting” our original mathematical expressing into a Julia expression. In the at form it is not evaluated yet, however we can always evaluate it via eval
.
julia> ex1=:((1/11) * 1000^11 + (1/2) * 1000^10 + (5/6) * 1000^9 - 1000^7 + 1000^5-(1/2) * 1000^3 + (5/66) * 1000) :((((((1 / 11) * 1000 ^ 11 + (1 / 2) * 1000 ^ 10 + (5 / 6) * 1000 ^ 9) - 1000 ^ 7) + 1000 ^ 5) - (1 / 2) * 1000 ^ 3) + (5 / 66) * 1000) julia> dump(ex1) Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol + 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol - 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol + 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol - 2: Expr 3: Expr 3: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol ^ 2: Int64 1000 3: Int64 5 3: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol * 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol / 2: Int64 1 3: Int64 2 3: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol ^ 2: Int64 1000 3: Int64 3 3: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol * 2: Expr head: Symbol call args: Array{Any}((3,)) 1: Symbol / 2: Int64 5 3: Int64 66 3: Int64 1000 julia> eval(ex1) -6.740310541071357e18
The output of dump
show the follow AST in all its glory (…well almost the depth is a bit truncated). Notice that here all our numbers are interpreted as Int64
.
Now we walk through the is AST and change all occurrences of Int64
with BigInt
by using the big
function.
function makeIntBig!(ex::Expr) args=ex.args for i in eachindex(args) if args[i] isa Int64 args[i]=big(args[i]) end if args[i] isa Expr makeIntBig!(args[i]) end end end julia> ex2=copy(ex1) :((((((1 / 11) * 1000 ^ 11 + (1 / 2) * 1000 ^ 10 + (5 / 6) * 1000 ^ 9) - 1000 ^ 7) + 1000 ^ 5) - (1 / 2) * 1000 ^ 3) + (5 / 66) * 1000) julia> makeIntBig!(ex2) julia> eval(ex2) 9.14099242414242434242419242425000000000000000000000000000000000000000000000014e+31
We see an improvement here, but the results are not very satisfactory. The divisions yield BigFloat
results, which had a tiny bit of floating point errors. Can we do better?
Julia has support for Rational
expressions baked in. We can use that improve the results. We just need to search for call
expressions the /
symbol and replace it by the //
symbol. For safety we just have to makes sure the operands are as subtype of Integer
.
function makeIntBig!(ex::Expr) args=ex.args if ex.head == :call && args[1]==:/ && length(args)==3 && all(x->typeof(x) <: Integer,args[2:end]) args[1]=:// args[2]=big(args[2]) args[3]=big(args[3]) else for i in eachindex(args) if args[i] isa Int64 args[i]=big(args[i]) end if args[i] isa Expr makeIntBig!(args[i]) end end end end julia> ex2=copy(ex1); julia> makeIntBig!(ex2) julia> eval(ex2) 91409924241424243424241924242500//1
Now that is much better! We have not lost any precision and we ended us with a Rational
expression.
Finally, we can build a macro
so the if we run into such expressions in the future and we want to evaluate them, we could just conveniently call it.
macro eval_bigInt(ex) makeIntBig!(ex) ex end
and we can now simply evaluate our original expression as
julia> @eval_bigInt (1/11) * 1000^11 + (1/2) * 1000^10 + (5/6) * 1000^9 - 1000^7 + 1000^5-(1/2) * 1000^3 + (5/66) * 1000 91409924241424243424241924242500//1