Metaohjelmointia Julialla, tekniikoista

Koska makrot ovat ennen kaikkea koodin esikäsittelyä, niiden inputit ovat rajalliset, sillä ohjelmakoodia ei ole suoritettu ennen makroa. Makro voi ottaa input argumentteina lähinnä numeroita ja merkkijonoja.

Tehdään yksinkertainen makro joka palauttaa inputin tyypin ja sisällön. Tällä voi helposti tutkia minkälaisia asioita makro “syö”:

macro showargs(args...)
    for arg in args
        @show typeof(arg), arg
    end
    return
end

@showargs(1, 1.0, true, a, "hei")
(typeof(arg), arg) = (Int64, 1)
(typeof(arg), arg) = (Float64, 1.0)
(typeof(arg), arg) = (Bool, true)
(typeof(arg), arg) = (Symbol, :a)
(typeof(arg), arg) = (String, "hei")

Listasta voi jotakin primitiivityyppejä puuttua, mutta tässä nämä pitkälti on. Kaikenlaiset monimutkaisemmat rakenteet eivät ole sellaisenaan käytettävissä makrossa sisällä, vaan ne menevät sisälle Expr-tyyppinä:

struct Foo end

@showargs(
    [1, 2, 3],
    :(a => 1),
    (1, 2, 3),
    (a = 1, b = 2),
    Dict(1 => 2),
    1 + 2,
    a + b,
    f(x) = x^2,
    rand(2, 2),
    Foo())
(typeof(arg), arg) = (Expr, :([1, 2, 3]))
(typeof(arg), arg) = (Expr, :(:(a => 1)))
(typeof(arg), arg) = (Expr, :((1, 2, 3)))
(typeof(arg), arg) = (Expr, :((a = 1, b = 2)))
(typeof(arg), arg) = (Expr, :(Dict(1 => 2)))
(typeof(arg), arg) = (Expr, :(1 + 2))
(typeof(arg), arg) = (Expr, :(a + b))
(typeof(arg), arg) = (Expr, :(f(x) = begin
            x ^ 2
        end))
(typeof(arg), arg) = (Expr, :(rand(2, 2)))
(typeof(arg), arg) = (Expr, :(Foo()))

Makroon voidaan myös laittaa begin .. end lohkon sisällä ohjelmakoodia, joka mahdollistaa periaatteessa pienimuotoisen konfiguraation tekemisen ennen varsinaista toiminallisuuta:

@showargs begin
    a = 1
    b = 2.0
    c = true
    d = "hei"
    e = [1, 2]
    f = (a => 1)
end
(typeof(arg), arg) = (Expr, quote
    a = 1
    b = 2.0
    c = true
    d = "hei"
    e = [1, 2]
    f = a => 1
end)

Myös key-value-pareja voi antaa, joka voisi olla makron konfiguroinnin lähtöpiste:

@showargs(
    coordinates = (1, 2),
    dims = 2,
    P = :(1, :u, :u^2))
(typeof(arg), arg) = (Expr, :(coordinates = (1, 2)))
(typeof(arg), arg) = (Expr, :(dims = 2))
(typeof(arg), arg) = (Expr, :(P = :((1, :u, :u ^ 2))))

Jos etsitään jotakin monimutkaisempaa, niin quote-tyyppinen ratkaisu voisi olla se paras tapa, sillä voidaan itse asiassa kirjoittaa ihan normaalia Julia-koodia joka sitten ajetaan makron ajovaiheessa eikä lopputuotteessa:

macro do_something_with_config(config)
    c = Module()
    Core.eval(c, config)
    return quote
        println("element family = ", $(c.element_family))
        println("c = ", $(c.c))
    end
end

@do_something_with_config begin
    element_family = "Lagrange"
    V = [1 2; 2 1]
    e = [1, 2]
    c = V * e
end
element family = Lagrange
c = [5, 4]

@macroexpand-komennolla voidaan taas tarkastaa, mitä makro oikeastaan tekee:

@macroexpand @do_something_with_config begin
    element_family = "Lagrange"
    V = [1 2; 2 1]
    e = [1, 2]
    c = V * e
end
quote
    Main.println("element family = ", "Lagrange")
    Main.println("c = ", [5, 4])
end

“Normaali” argumentin syöttäminen makroon sisälle ei toimi. Näin voi tehdä:

macro evalf(f, vars)
    @show f
    return :(($vars) -> $f)
end

@macroexpand @evalf 1 + u + v + 2*u*v (u, v)
f = :(1 + u + v + 2 * u * v)

:((var"#4#u", var"#5#v")->begin
            1 + var"#4#u" + var"#5#v" + 2 * var"#4#u" * var"#5#v"
        end)

Mutta ei näin:

expr = :(1 + u + v)
@macroexpand @evalf expr (u, v)
f = :expr

:((var"#6#u", var"#7#v")->begin
        Main.expr
    end)

Makro on tietoinen omasta ympäristöstä:

macro evalf2(f)
    @show f
    return :($f)
end

@macroexpand @evalf2 1 + u + v + 2*u*v
f = :(1 + u + v + 2 * u * v)

:(1 + Main.u + Main.v + 2 * Main.u * Main.v)

Evaluointi yrittää käyttää globaaleja muuttujia, koska u tai v ei ole määritelty makrossa. Mikäli ne määritellään makron sisällä, kaikki toimii kuten pitääkin.

macro evalf3(f)
    @show f
    return quote
        u = 1
        v = 2
        $f
    end
end

@macroexpand @evalf3 1 + u + v + 2*u*v
f = :(1 + u + v + 2 * u * v)

quote
    var"#10#u" = 1
    var"#11#v" = 2
    1 + var"#10#u" + var"#11#v" + 2 * var"#10#u" * var"#11#v"
end

Evaluointi tapahtuu aina globaalilla tasolla riippumatta onko makroa käytetty funktion sisällä:

function do_evalf2()
    u = 1
    v = 2
    @evalf2 u + v
end

do_evalf2()
f = :(u + v)

UndefVarError: u not defined

Stacktrace:
    [1] do_evalf2() at ./In[12]:4
    [2] top-level scope at In[12]:6

Jos määritellään u ja v globaalisti niin homma toimii:

u = 5
v = 10
do_evalf2()
15

Näin siksi, että u ja v ovat itse asiassa Main.u ja Main.v.

Juliasta löytyy @eval-makro suoraan, jota voi käyttää Expr-tyyppien evaluoimiseen. Esimerkiksi:

expr = :(1 + u + v + u*v)
@eval h(u, v) = $expr

h(1, 2)
6

Expr voidaan myös laskea ja palauttaa funktiosta, joka voidaan sitten evaluoida inline-funktioksi:

function get_stuff()
    return :(t(u, v) = 99)
end

@eval $(get_stuff())

t(2, 3)
99

Kaikki voidaan myös tehdä funktion sisällä, mutta evaluointi tapahtuu globaaliin ympäristöön:

function gen_func(fname, fval)
    fname2 = Symbol("$fname$fname")
    fval2 = 2*fval
    expr = :($fname2() = $fval2)
    @eval $expr
end

gen_func(:testi, 123)

testitesti()
246