Defining macros

In previous chapters, we have already used macros, such as @printf, in Chapter 2, Variables, Types, and Operations, and @time in Chapter 3, Functions. Macros are like functions, but instead of values they take expressions (which can also be symbols or literals) as input arguments. When a macro is evaluated, the input expression is expanded, that is, the macro returns a modified expression. This expansion occurs at parse time when the syntax tree is being built, not when the code is actually executed.

The following descriptions highlight the difference between macros and functions when they are called or invoked:

  • Function: It takes the input values and returns the computed values at runtime
  • Macro: It takes the input expressions and returns the modified expressions at parse time

In other words, a macro is a custom program transformation. Macros are defined with the keyword as follows:

macro mname 
# code returning expression end

It is invoked as @mname exp1 exp2 or @mname(exp1, exp2) (the @ sign distinguishes it from a normal function call). The macro block defines a new scope. Macros allow us to control when the code is executed.

Here are some examples:

  • A first simple example is macint macro, which does the interpolation of its argument expression ex:
# see the code in Chapter 7macros.jl) 
macro macint(ex) 
    quote 
        println("start") 
        $ex 
        println("after") 
    end 
end 

@macint println("Where am I?") will result in:

start
Where am I?
after
  • The second example is an assert macro that takes an expression ex and tests whether it is true or not; in the last case, an error is thrown:
macro assert(ex) 
:($ex ? nothing : error("Assertion failed: ",  
      $(string(ex)))) 
end

For example: @assert 1 == 1.0 returns nothing. @assert 1 == 42 returns ERROR: Assertion failed: 1 == 42.

The macro replaces the expression with a ternary operator expression, which is evaluated at runtime. To examine the resulting expression, use the macroexpand function as follows:

macroexpand(Main, :(@assert 1 == 42))

This returns the following expression:

:(if 1 == 42
        nothing
  else
        (Base.throw)((Base.AssertionError)("1 == 42"))
  end)

This assert function is just a macro example, using the built-in assert function in the production code. (Refer to the Testing subsection of the Built-in macros section.)

  • The third example mimics an unless construct, where branch is executed if the condition test_cond is not true:
macro unless(test_cond, branch) 
    quote 
        if !$test_cond 
            $branch 
        end 
    end 
end 

Suppose arr = [3.14, 42, 'b'], then @unless 42 in arr println("arr does not contain 42") returns nothing, but @unless 41 in arr println("arr does not contain 41") prints out the following command:

arr does not contain 41

Here, macroexpand(Main, :(@unless 41 in arr println("arr does not contain 41"))) returns the following output:

quote
#= REPL[49]:3 =#
if !(41 in Main.arr)
#= REPL[49]:4 =#
(Main.println)("arr does not contain 41")
end
end

Unlike functions, macros inject the code directly into the namespace in which they are called, possibly this is also in a different module than the one in which they were defined. It is therefore important to ensure that this generated code does not clash with the code in the module in which the macro is called. When a macro behaves appropriately like this, it is called a hygienic macro. The following rules are used when writing hygienic macros:

  • Declare the variables used in the macro as local, so as not to conflict with the outer variables
  • Use the escape function esc to make sure that an interpolated expression is not expanded, but instead is used literally
  • Don't call eval inside a macro (because it is likely that the variables you are evaluating don't even exist at that point)

These principles are applied in the following timeit macro, which times the execution of an expression ex (like the built-in macro @time):

macro timeit(ex) 
    quote 
        local t0 = time()    
        local val = $(esc(ex)) 
        local t1 = time()    
        print("elapsed time in seconds: ") 
        @printf "%.3f" t1 - t0 
        val 
    end 
end 

The expression is executed through $, and t0 and t1 are respectively the start and end times.

@timeit factorial(10) returns elapsed time in seconds: 0.0003628800.

@timeit a^3 returns elapsed time in seconds: 0.0013796416.

Hygiene with macros is all about differentiating between the macro context and the calling context.

Macros are valuable tools which save you a lot of tedious work, and, with the quoting and interpolation mechanism, they are fairly easy to create. You will see them being used everywhere in Julia for lots of different tasks. Ultimately, they allow you to create domain-specific languages (DSLs). To get a better idea of this concept, we suggest you experiment with the other examples in the accompanying code file.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.143.255.36