Macro problem

I would like to compute the first n digits of some number x in base b. This looks like a job for a macro since the argument should be evaluated only after the precision has been set.

macro numbers(x, b, n = 100)
  setprecision(round(Integer, n)) do
     return eval(:(reverse(digits(floor(BigInt, big(2)^$n*$x), base = $b))))
  end
end

julia> join(@numbers(sqrt(big(2)), 2))
"10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110"

Fine, this is the expected result. Unfortunately

julia> [join(@numbers(sqrt(big(2)),2, u)) for u in [10,20,30,40]]
ERROR: LoadError: UndefVarError: u not defined

I am not sure how to fix this. Any hint?

Is chiffres the same macro as your numbers above?
Since u is not global, I think you’re missing an esc(...) somewhere. You can look at the generated code with @macroexpand and search for an error in the generated code. I’m not in front of a computer atm so I can’t look into it in more detail right now.

Sorry, this is not an answer to your question, but are you absolutely sure that it cannot be solved without metaprogramming? This problem seems too simple to use such a technique.

Maybe this discourse thread can be of interest: How to warn new users away from metaprogramming there are some good proposals, how one can solve problems in base Julia, without turning to macros and other things.

3 Likes

Indeed. Typo corrected.

Feel free to contribute a macro free solution. The problem is not simple though. Most programming languages need an external library for this job.

Macros are not magic, they are just manipulating AST, so they can always be presented with some pure Julia calculations. Maybe I do not understand what problem you are trying to solve here, but what is wrong with usual function?

function bignums(x, b, n = 100)
    setprecision(round(Integer, n)) do
        reverse(digits(floor(BigInt, big(2)^n*x), base = b))
    end
end

julia> join(bignums(sqrt(big(2)), 2))
"10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001
101110"

julia> @assert join(bignums(sqrt(big(2)), 2)) == join(@numbers(sqrt(big(2)), 2))

julia> [join(bignums(sqrt(big(2)), 2, u)) for u in [10,20,30,40]]
4-element Vector{String}:
 "10110101000"
 "101101010000010011110"
 "1011010100000100111100110011010"
 "10110101000001001111001100110011111110100"

Only difference with macro that I can see is

julia> @macroexpand @numbers(sqrt(big(2)), 2)
101-element Vector{Int64}:
 1
 0
 1
 1
 0
 â‹®

so it is calculated at compile-time, instead of runtime. This is rather bad, because it is abusing compiler and definitely can’t be applied to runtime calculations like [join(@numbers(sqrt(big(2)),2, u)) for u in [10,20,30,40]].

1 Like

@skoffer Your function does not return the correct result.

julia> join(@numbers(sqrt(big(2)), 2, 1000))
"10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110101010010101011111010011111000111010110111101100000101110101000100100111011101010000100110011101101000101111010110010000101100000110011001110011001000101010100101011111100100000110000010000111010101110001010001011000011101010001011000111111110011011111101110010000011110110110011100100001111011101001010100001011110010000111001110001111011010010100111100000000100100001110011011000111101111110100010011101101000110100100010000000101110100001110100001010101111000111110100111001010011000001011001110001100000000100011011110000110011011110111100101010110001101111001001000100010110100010000100010110001010010001100000101010111100011100100010111101111100010011100011001111000110110101011010100010100011100010111011011111101001110111001100101100101010011000110100001100110001111100111100100001001101111101010010111100010010000011111000001101101110010110000010111011101010101001001010000010001001100100000"

julia> join(bignums(sqrt(big(2)), 2), 1000)
"110000100011000110000100011000010001100001000010000100001000010001100001000010001100011000110001100001000010001100011000010000100011000110000100001000110001100011000110001100011000110000100001000110001100011000010001100011000110001100001000010001100011000010000100011000010000100011000010000100001000010001100001000010000100011000010001100011000010000100011000010001100011000110001100011000010001100011000010000100001000110000100001000110001100001000110001100001000010001100011000010001100011000110000"

You don’t understand that what is passed to the body of the function is the result of evaluating the argument in the current environment, which comes with a given set precision. The default is

julia> precision(sqrt(big(2)))
256

So a function having sqrt(big(2)) as an argument receives 256 binary digits, that’s all. A macro does not evaluate its argument, hence it can set a different environment in which the expression defining its argument can be evaluated. Look at the last digits after the 256th in the next evaluation.

julia> join(bignums(sqrt(big(2)), 2, 300))
"1011010100000100111100110011001111111001110111100110010010000100010110010111110110001001101100110111010101001010101111101001111100011101011011110110000010111010100010010011101110101000010011001110110100010111101011001000010110000011001100111001100100010101000000000000000000000000000000000000000000000"

So, as you see, macros are magic.

Let us agree to disagree :slight_smile: There is no magic here and there are multiple ways to solve this issue.

First of all, as you have said the reason, why the bignums function is not working, related to the fact that sqrt(big(2)) is calculated before internal calculations are being made, and it is calculated with default precision, which is equal 256. So the easiest way to overcome this issue is to calculate this function with the necessary precision and here one can use higher order functions

julia> @assert join(bignums(setprecision(() -> sqrt(big(2)), 1000), 2, 1000)) == join(@numbers(sqrt(big(2)), 2, 1000))

julia> [join(bignums(setprecision(() -> sqrt(big(2)), u), 2, u)) for u in [10, 20, 30, 40, 1000]
]
5-element Vector{String}:
 "10110101000"
 "101101010000010011110"
 "1011010100000100111100110011010"
 "10110101000001001111001100110011111110100"
 "10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110101010010101011111010011111000111010110111101100000101110101000100100111011101010000100110011101101000101111010110010000101100000110011001110011001000101010100101011111100100000110000010000111010101110001010001011000011101010001011000111111110011011111101110010000011110110110011100100001111011101001010100001011110010000111001110001111011010010100111100000000100100001110011011000111101111110100010011101101000110100100010000000101110100001110100001010101111000111110100111001010011000001011001110001100000000100011011110000110011011110111100101010110001101111001001000100010110100010000100010110001010010001100000101010111100011100100010111101111100010011100011001111000110110101011010100010100011100010111011011111101001110111001100101100101010011000110100001100110001111100111100100001001101111101010010111100010010000011111000001101101110010110000010111011101010101001001010000010001001100100000"

Not very convenient? Yes. Impossible? No.

One can make it somewhat easier by applying the same idea of high order function to bignums itself.

function bignums2(f, b, n = 100)
    setprecision(round(Integer, n)) do
        reverse(digits(floor(BigInt, big(2)^n*f()), base = b))
    end
end

julia> @assert join(bignums2(() -> sqrt(big(2)), 2, 1000)) == join(@numbers(sqrt(big(2)), 2, 1000))
julia> [join(bignums2(() -> sqrt(big(2)), 2, u)) for u in [10, 20, 30, 40, 1000]]
5-element Vector{String}:
 "10110101000"
 "101101010000010011110"
 "1011010100000100111100110011010"
 "10110101000001001111001100110011111110100"
 "10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110101010010101011111010011111000111010110111101100000101110101000100100111011101010000100110011101101000101111010110010000101100000110011001110011001000101010100101011111100100000110000010000111010101110001010001011000011101010001011000111111110011011111101110010000011110110110011100100001111011101001010100001011110010000111001110001111011010010100111100000000100100001110011011000111101111110100010011101101000110100100010000000101110100001110100001010101111000111110100111001010011000001011001110001100000000100011011110000110011011110111100101010110001101111001001000100010110100010000100010110001010010001100000101010111100011100100010111101111100010011100011001111000110110101011010100010100011100010111011011111101001110111001100101100101010011000110100001100110001111100111100100001001101111101010010111100010010000011111000001101101110010110000010111011101010101001001010000010001001100100000"

If your numbers a result of some function, then it is better to follow this approach instead of falling down to metaprogramming. I mean in real life code, instead of () -> sqrt(big(2)) you can use actual function, which produces some BigInt values.

Now, if it is absolutely necessary to use a macro, then one can do something like this

macro numbers2(x, b, n = 100)
    return quote 
        setprecision(round(Integer, $(esc(n)))) do
            reverse(digits(floor(BigInt, big(2)^$(esc(n))*$(esc(x))), base = $(esc(b))))
        end
    end
end

julia> @assert join(@numbers2(sqrt(big(2)), 2, 1000)) == join(@numbers(sqrt(big(2)), 2, 1000))
julia> [join(@numbers2(sqrt(big(2)), 2, u)) for u in [10, 20, 30, 40, 1000]]
5-element Vector{String}:
 "10110101000"
 "101101010000010011110"
 "1011010100000100111100110011010"
 "10110101000001001111001100110011111110100"
 "10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110101010010101011111010011111000111010110111101100000101110101000100100111011101010000100110011101101000101111010110010000101100000110011001110011001000101010100101011111100100000110000010000111010101110001010001011000011101010001011000111111110011011111101110010000011110110110011100100001111011101001010100001011110010000111001110001111011010010100111100000000100100001110011011000111101111110100010011101101000110100100010000000101110100001110100001010101111000111110100111001010011000001011001110001100000000100011011110000110011011110111100101010110001101111001001000100010110100010000100010110001010010001100000101010111100011100100010111101111100010011100011001111000110110101011010100010100011100010111011011111101001110111001100101100101010011000110100001100110001111100111100100001001101111101010010111100010010000011111000001101101110010110000010111011101010101001001010000010001001100100000"

First of all, it returns quote instead of eval, so macro is doing what it should do, i.e. transforming code instead of calculating in compile time. Secondly, I use macro hygene to generate proper code. You can use @macroexpand to verify the result.

julia> @macroexpand [join(@numbers2(sqrt(big(2)), 2, u)) for u in [10, 20, 30, 40, 1000]]
:([join(begin
          #= REPL[25]:3 =#
          Main.setprecision(Main.round(Main.Integer, u)) do
              #= REPL[25]:4 =#
              Main.reverse(Main.digits(Main.floor(Main.BigInt, Main.big(2) ^ u * sqrt(big(2))),
base = 2))
          end
      end) for u = [10, 20, 30, 40, 1000]])

You can compare this code with the macro without hygene

macro numbers3(x, b, n = 100)
    return quote 
        setprecision(round(Integer, $n)) do
            reverse(digits(floor(BigInt, big(2)^$n*$x), base = $b))
        end
    end
end

julia> @macroexpand [join(@numbers3(sqrt(big(2)), 2, u)) for u in [10, 20, 30, 40, 1000]]
:([join(begin
          #= REPL[40]:3 =#
          Main.setprecision(Main.round(Main.Integer, Main.u)) do
              #= REPL[40]:4 =#
              Main.reverse(Main.digits(Main.floor(Main.BigInt, Main.big(2) ^ Main.u * Main.sqrt(
Main.big(2))), base = 2))
          end
      end) for u = [10, 20, 30, 40, 1000]])

and from this code, it is easy to understand why Julia throw error.

4 Likes

You could be a little friendlier :slight_smile:

that can’t work because you misplaced the ).
Nevertheless, you are correct. The function version truncates everything after the standard precision to 0s.

The important part here is, that you want setprecisioon to act on sqrt(big(2)) already. If you insist on that syntax, it’s indeed a case for a macro (as far as I see).

The answer to your question:
What you want to do is to evaluate esc(n) because the macro doesn’t see the value of u in a loop but the symbol :u itself, so macro hygiene kicks in if you use u (or n in the macro) directly.

julia> macro numbers(x, b, n = 100)
         return quote
         precision = $(esc(n))
         setprecision(round(Integer, precision)) do
            reverse(digits(floor(BigInt, big(2)^precision*$x), base = $b))
         end
         end
       end
@numbers (macro with 2 methods)

which does seem to work:

julia> [join(@numbers(sqrt(big(2)), 2, u)) for u in (10,20,30,100)]
4-element Vector{String}:
 "10110101000"
 "101101010000010011110"
 "1011010100000100111100110011010"
 "10110101000001001111001100110011111110011101111001100100100001000101100101111101100010011011001101110"

Everything’s magic around here :slight_smile:

PS: @Skoffer was faster, so sorry for the redundancy. I agree that a macro for that specialized case is probably not worthwhile. But having a macro to set the precision for some statement as a whole may be a nice convenience option.

3 Likes

Just because you marked my answer as solution:
If you want to use that macro, use

From Skoffer. All variables are escaped from hygiene there, which means that you won’t have a problem if you alter x or b in a loop, which would probably fail for the same reason you opened this question with my version.

1 Like