Meta-ness

Julia allows us to talk in a "meta" way ("one level up"), about Julia code, that is to "treat code as data" and manipulate it as just another object in Julia. (This is very similar to Lisp.)

The basic objects in this approach are unevaluated symbols:


In [3]:
:a  # "the symbol a"


Out[3]:
:a

In [4]:
typeof(:a)


Out[4]:
Symbol

:a refers to the symbol a. We can evaluate it with the eval function:


In [5]:
eval(:a)


a not defined
while loading In[5], in expression starting on line 1

a must be defined for this to work:


In [6]:
a = 3


Out[6]:
3

In [7]:
eval(:a)


Out[7]:
3

The eval function takes an expression and evaluates it, that is, generates the corresponding code

Everything is a symbol:


In [8]:
:+, :sin


Out[8]:
(:+,:sin)

In [9]:
typeof(:+)


Out[9]:
Symbol

Symbols may be combined into expressions, which are the basic objects that represent pieces of Julia code:


In [10]:
ex = :(a + b)  # the expression 'a+b'


Out[10]:
:(a + b)

In [11]:
typeof(ex)


Out[11]:
Expr

In [12]:
ex


Out[12]:
:(a + b)

In [13]:
b = 7
eval(ex)


Out[13]:
10

An expression is just a Julia object, so we can introspect (find out information about it):


In [60]:
names(ex)


Out[60]:
3-element Array{Symbol,1}:
 :head
 :args
 :typ 

In [61]:
# ex.<TAB>

In [14]:
ex.head


Out[14]:
:call

In [63]:
ex.args


Out[63]:
3-element Array{Any,1}:
 :+
 :a
 :b

In [64]:
ex.typ


Out[64]:
Any

More complicated expressions are represented as "abstract syntax trees" (ASTs), consisting of expressions nested inside expressions:


In [15]:
ex = :( sin(3a + 2b^2) )


Out[15]:
:(sin(3a + 2 * b^2))

In [16]:
ex.args


Out[16]:
2-element Array{Any,1}:
 :sin           
 :(3a + 2 * b^2)

In [17]:
typeof(ex.args[2])


Out[17]:
Expr

In [18]:
ex.args[2].args


Out[18]:
3-element Array{Any,1}:
 :+        
 :(3a)     
 :(2 * b^2)

Expressions can be arbitrary Julia code that when evaluated will have side effects. For longer blocks of code, quote...end may be used instead of :( ... )


In [19]:
ex2 = 
quote
    y = 3
    z = sin(y+1)
end


Out[19]:
:(begin  # In[19], line 3:
        y = 3 # line 4:
        z = sin(y + 1)
    end)

In [70]:
y


y not defined
while loading In[70], in expression starting on line 1

In [71]:
eval(ex2)
z


Out[71]:
-0.7568024953079282

The full form of the abstract syntax tree in a style similar to a Lisp s-expression can be obtained using functions from the Meta module in Base:


In [20]:
Meta.show_sexpr(ex2)


(:block,
  (:line, 3, :In[19]),
  (:(=), :y, 3),
  :( # line 4:),
  (:(=), :z, (:call, :sin, (:call, :+, :y, 1)))
)

Another way of seeing the structure is with dump:


In [21]:
dump(ex2)


Expr 
  head: Symbol block
  args: Array(Any,(4,))
    1: Expr 
      head: Symbol line
      args: Array(Any,(2,))
        1: Int64 3
        2: Symbol In[19]
      typ: Any
    2: Expr 
      head: Symbol =
      args: Array(Any,(2,))
        1: Symbol y
        2: Int64 3
      typ: Any
    3: LineNumberNode 
      line: Int64 4
    4: Expr 
      head: Symbol =
      args: Array(Any,(2,))
        1: Symbol z
        2: Expr 
          head: Symbol call
          args: Array(Any,(2,))
          typ: Any
      typ: Any
  typ: Any

Macros

With the ability to think of code in terms of a data structure in the Julia language, we can now manipulate those data structures, allowing us to create Julia code on the fly from within Julia. This is known as metaprogramming: programming a program.

The name macro is given to a kind of "super-function" that takes a piece of code as an argument, and returns an altered piece of code. A macro is thus a very different kind of object than a standard function. [Although it can be thought of as a function in the mathematical sense of the word.]

The Julia manual puts it like this:

macros map a tuple of argument expressions to a returned expression

Although metaprogramming is possible in many languages (including Python), Julia makes it particularly natural (although not exactly "easy"!)

Metaprogramming is useful in a variety of settings:

  • to eliminate boilerplate (repetitive) code
  • to automatically generate complex code that would be painful by hand
  • to unroll loops for efficiency
  • to inline code for efficiency

Macros are invoked using the @ sign, e.g.


In [22]:
@time sin(10)


elapsed time: 0.004374081 seconds (47856 bytes allocated)
Out[22]:
-0.5440211108893698

A trivial example of defining a macro is the following, which duplicates whatever code it is passed. The $ sign is used to interpolate the value of the expression (similar to its usage for string interpolation):


In [23]:
macro duplicate(ex)
    quote
        $ex
        $ex
    end
end

In [24]:
@duplicate println(sin(10))


-0.5440211108893698
-0.5440211108893698

In [25]:
ex = :(@duplicate println(sin(10)))


Out[25]:
:(@duplicate println(sin(10)))

In [77]:
eval(ex)


-0.5440211108893698
-0.5440211108893698

In [78]:
typeof(ex)


Out[78]:
Expr

We can see what effect the macro actually has using macroexpand:


In [26]:
macroexpand(ex)


Out[26]:
:(begin  # In[23], line 3:
        println(sin(10)) # line 4:
        println(sin(10))
    end)

In [27]:
macroexpand(:(@time sin(10)))


Out[27]:
:(begin  # util.jl, line 50:
        local #261#b0 = Base.gc_bytes() # line 51:
        local #262#t0 = Base.time_ns() # line 52:
        local #263#g0 = Base.gc_time_ns() # line 53:
        local #264#val = sin(10) # line 54:
        local #265#g1 = Base.gc_time_ns() # line 55:
        local #266#t1 = Base.time_ns() # line 56:
        local #267#b1 = Base.gc_bytes() # line 57:
        Base.time_print(Base.-(#266#t1,#262#t0),Base.-(#267#b1,#261#b0),Base.-(#265#g1,#263#g0)) # line 58:
        #264#val
    end)

Exercise: Define a macro @until that does an until loop.



In [30]:
macro until(expr1, expr2)
    quote
    #:(
        while !($expr1)  # code interpolation
            $expr2
        end
    #)
    end
end

In [34]:
i = 0
@until i==10 begin
    println(i)
    i += 1
end


0
1
2
3
4
5
6
7
8
9

A nontrivial example: Horner's method

There are many interesting examples of macros in Base. One that is accessible is Horner's method for evaluating a polynomial:

$$p(x) = a_n x^n + a_{n-1} x^{n-1} + \cdots + a_1 x^1 + a_0$$

may be evaluated efficiently as

$$p(x) = a_0 + x(a_1 + \cdots x(a_{n-2} + \cdots + x(a_{n-1} + x a_n) \cdots ) ) $$

with only $n$ multiplications.

The obvious way to do this is with a for loop. But if we know the polynomial at compile time, this loop may be unrolled using metaprogramming. This is implemented in the Math module in math.jl in Base, so the name of the macro (which is not exported) is @Base.Math.horner


In [35]:
horner


horner not defined
while loading In[35], in expression starting on line 1

In [37]:
# copied from base/math.jl
macro horner(x, p...)
    ex = esc(p[end])
    for i = length(p)-1:-1:1
        ex = :( $(esc(p[i])) + t * $ex )
    end
    Expr(:block, :(t = $(esc(x))), ex)
end

This is called as follows: to evaluate the polynomial $p(x) = 2 + 3x + 4x^2$ at $x=3$, we do


In [38]:
x = 3
@horner(x, 2, 3, 4)


Out[38]:
47

[Even though the Horner macro is not exported in Base, we can access it as @Base.Math.horner]

To see what the macro does to this call, we again use macroexpand:


In [39]:
macroexpand(:(@horner(x, 2, 3, 4)))


Out[39]:
:(begin 
        #269#t = x
        2 + #269#t * (3 + #269#t * 4)
    end)

In [88]:
macroexpand(:(@Base.Math.horner(x, 2, 3, 4)))


Out[88]:
:(begin 
        #292#t = x
        Base.Math.+(2,Base.Math.*(#292#t,Base.Math.+(3,Base.Math.*(#292#t,4))))
    end)

In [101]:
x = 3.5
@printf("%.5f", x)


3.50000

In [2]:
@printf


@printf: called with zero arguments
while loading In[2], in expression starting on line 1

In [102]:
ex = :(@time sin(10))


Out[102]:
:(@time sin(10))

In [103]:
Meta.show_sexpr(ex)


(:macrocall, :@time, (:call, :sin, 10))

In [5]:
xdump(ex)


Expr 
  head: Symbol macrocall
  args: Array(Any,(2,))
    1: Symbol @time
    2: Expr 
      head: Symbol call
      args: Array(Any,(2,))
        1: Symbol sin
        2: Int64 10
      typ: Any::DataType  <: Any
  typ: Any::DataType  <: Any

In [6]:
macroexpand(ex)


Out[6]:
:(begin  # util.jl, line 50:
        local #254#b0 = Base.gc_bytes() # line 51:
        local #255#t0 = Base.time_ns() # line 52:
        local #256#g0 = Base.gc_time_ns() # line 53:
        local #257#val = sin(10) # line 54:
        local #258#g1 = Base.gc_time_ns() # line 55:
        local #259#t1 = Base.time_ns() # line 56:
        local #260#b1 = Base.gc_bytes() # line 57:
        Base.time_print(Base.-(#259#t1,#255#t0),Base.-(#260#b1,#254#b0),Base.-(#258#g1,#256#g0)) # line 58:
        #257#val
    end)

In [7]:
@time sin(10)


elapsed time: 0.006097425 seconds (47856 bytes allocated)
Out[7]:
-0.5440211108893698

In [8]:
dump(ex)


Expr 
  head: Symbol macrocall
  args: Array(Any,(2,))
    1: Symbol @time
    2: Expr 
      head: Symbol call
      args: Array(Any,(2,))
        1: Symbol sin
        2: Int64 10
      typ: Any
  typ: Any

In [9]:
xdump(ex)


Expr 
  head: Symbol macrocall
  args: Array(Any,(2,))
    1: Symbol @time
    2: Expr 
      head: Symbol call
      args: Array(Any,(2,))
        1: Symbol sin
        2: Int64 10
      typ: Any::DataType  <: Any
  typ: Any::DataType  <: Any

In [8]:
type Vector2D{T <: Real}
    x::T
    y::T
end

In [9]:
methods(Vector2D)


Out[9]:
1 method for generic function Vector2D:
  • Vector2D{T<:Real}(x::T<:Real,y::T<:Real)

In [10]:
v = Vector2D{Float64}(3, 4)


Out[10]:
Vector2D{Float64}(3.0,4.0)

In [11]:
v = Vector2D(3., 4)


no method Vector2D{T<:Real}(Float64, Int64)
while loading In[11], in expression starting on line 1

In [18]:
v = Vector2D(3., 4.)


Out[18]:
Vector2D{Float64}(3.0,4.0)

In [2]:
# clear

In [4]:
# uses show(io::IO, v::DataType) by default

In [15]:
typeof(Vector2D)


Out[15]:
DataType

In [13]:
super(Vector2D)


Out[13]:
Any

In [3]:
methods(show)


Out[3]:
89 methods for generic function show:

In [20]:
Base.show(io::IO, v::Vector2D) = print(io, "[$(v.x), $(v.y)]")


Out[20]:
show (generic function with 91 methods)

In [18]:
v


Out[18]:
[3.0, 4.0]

In [19]:
Vector2D(3im, 4im)


no method Vector2D{T<:Real}(Complex{Int64}, Complex{Int64})
while loading In[19], in expression starting on line 1

In [29]:
code = 
"""
function testinf(a, b)
    y = a + b
    return sin(y)
end
"""
ex = parse(code)
eval(ex)


Out[29]:
testinf (generic function with 1 method)

In [21]:
function testinf(a, b)
    y = a + b
    return sin(y)
end


Out[21]:
testinf (generic function with 1 method)

In [24]:
code_lowered(testinf,(Int, Int))


Out[24]:
1-element Array{Any,1}:
 :($(Expr(:lambda, {:a,:b}, {{:y},{{:a,:Any,0},{:b,:Any,0},{:y,:Any,18}},{}}, :(begin  # In[21], line 2:
        y = a + b # line 3:
        return sin(y)
    end))))

In [22]:
code_typed(testinf, (Int, Int))


Out[22]:
1-element Array{Any,1}:
 :($(Expr(:lambda, {:a,:b}, {{:y,:_var1},{{:a,Int64,0},{:b,Int64,0},{:y,Int64,18},{:_var1,Float64,18}},{}}, :(begin  # In[21], line 2:
        y = top(box)(Int64,top(add_int)(a::Int64,b::Int64))::Int64 # line 3:
        _var1 = GetfieldNode(Base.Math,:box,Any)(Float64,top(sitofp)(Float64,y::Int64))::Float64
        return GetfieldNode(Base.Math,:nan_dom_err,Any)(top(ccall)($(Expr(:call1, :(top(tuple)), "sin", GetfieldNode(Base.Math,:libm,Any)))::(ASCIIString,ASCIIString),Float64,$(Expr(:call1, :(top(tuple)), :Float64))::(Type{Float64},),_var1::Float64,0)::Float64,_var1::Float64)::Float64
    end::Float64))))

In [23]:
code_typed(testinf, (Float64, Float64))


Out[23]:
1-element Array{Any,1}:
 :($(Expr(:lambda, {:a,:b}, {{:y},{{:a,Float64,0},{:b,Float64,0},{:y,Float64,18}},{}}, :(begin  # In[21], line 2:
        y = top(box)(Float64,top(add_float)(a::Float64,b::Float64))::Float64 # line 3:
        return GetfieldNode(Base.Math,:nan_dom_err,Any)(top(ccall)($(Expr(:call1, :(top(tuple)), "sin", GetfieldNode(Base.Math,:libm,Any)))::(ASCIIString,ASCIIString),Float64,$(Expr(:call1, :(top(tuple)), :Float64))::(Type{Float64},),y::Float64,0)::Float64,y::Float64)::Float64
    end::Float64))))

In [25]:
code_llvm(testinf, (Int, Int))


define double @"julia_testinf;19510"(i64, i64) {
top:
  %2 = add i64 %1, %0, !dbg !2501
  %3 = sitofp i64 %2 to double, !dbg !2502
  %4 = call double inttoptr (i64 4514612992 to double (double)*)(double %3), !dbg !2502
  %5 = fcmp ord double %4, 0.000000e+00, !dbg !2502
  %6 = fcmp uno double %3, 0.000000e+00, !dbg !2502
  %7 = or i1 %5, %6, !dbg !2502
  br i1 %7, label %pass, label %fail, !dbg !2502

fail:                                             ; preds = %top
  %8 = load %jl_value_t** @jl_domain_exception, align 8, !dbg !2502, !tbaa %jtbaa_const
  call void @jl_throw_with_superfluous_argument(%jl_value_t* %8, i32 3), !dbg !2502
  unreachable, !dbg !2502

pass:                                             ; preds = %top
  ret double %4, !dbg !2502
}

In [26]:
code_native(testinf, (Int, Int))


	.section	__TEXT,__text,regular,pure_instructions
Filename: In[21]
Source line: 2
	push	RBP
	mov	RBP, RSP
Source line: 2
	sub	RSP, 16
	add	RDI, RSI
Source line: 3
	vcvtsi2sd	XMM0, XMM0, RDI
	vmovsd	QWORD PTR [RBP - 8], XMM0
	movabs	RAX, 4514612992
	call	RAX
	vucomisd	XMM0, XMM0
	jp	6
	add	RSP, 16
	pop	RBP
	ret
	vmovsd	XMM1, QWORD PTR [RBP - 8]
	vucomisd	XMM1, XMM1
	jp	-21
	movabs	RAX, 4380089384
	mov	RDI, QWORD PTR [RAX]
	movabs	RAX, 4367555552
	mov	ESI, 3
	call	RAX

In [32]:
names(testinf)


Out[32]:
3-element Array{Symbol,1}:
 :fptr
 :env 
 :code

In [34]:
testinf.fptr


Out[34]:
Ptr{Void} @0x00000001044f2e50

In [38]:
testinf.code


access to undefined reference
while loading In[38], in expression starting on line 1

In [11]:
?dump


INFO: Loading help data...
Base.dump(x)

   Show all user-visible structure of a value.

Possible solution of the @until exercise:


In [2]:
macro until(ex1, ex2)
    quote
        while !($ex1)
            $ex2
        end
    end
end

In [10]:
i = 1

@until i > 10 begin
    println(i)
    i += 1
end


1
2
3
4
5
6
7
8
9
10

In [11]:
i = 1

@until i > 10 
begin
    println(i)
    i += 1
end


wrong number of arguments
while loading In[11], in expression starting on line 4

In [13]:
:while


Out[13]:
:while

In [ ]: