Julian makrot

Eräs hieno ominaisuus Juliassa on sen makrot. Juliassa makro on hieman funktion kaltainen rakenne, joka palauttaa Expr-tyypin joka suoritetaan makron päätyttyä. Makroa voi ajatella ikääneräänlaisena esikäsittelijänä, jolla voi muokata koodin rakennetta ennenkuin se itse asiassa suoritetaan.

Tarkastellaan seuraavanlaista funktiota:

function print_sum_f(a, b)
    c = a + b
    println("Tulostetaan lukujen $a ja $b summa eli $c")
    println(c)
end

print_sum_f(1, 2)

Funktio laskee ajoaikana muuttujat a ja b yhteen ja tulostaa niiden summan. Tämä on varsin hyvä, jos muuttujia a ja b ei tunneta ennen kuin koodia käännetään. Mutta jos tunnetaan, voimme itse asiassa laskea summan jo ohjelman käännösvaiheessa. Tämä onnistuu kirjoittamalla makro:

macro print_sum_m(a, b)
    c = a + b
    println("Tulostetaan lukujen $a ja $b summa eli $c")
    return :(println($c))
end

@print_sum_m(1, 2)

Kun koodia ajetaan, niin saadaan molemmissa tapauksissa tääysin sama tuloste:

Tulostetaan lukujen 1 ja 2 summa eli 3
3

Se mitenkä nämä ratkaisut eroavat toisistansa on, että siinä missä ensimmäisessä vaihtoehdossa lasketaan 1 + 2 ajon aikana, makrossa se tapahtuu käännösaikana. Makro kirjoittaa Julian ohjelmakoodiin rivin println(3) siihen paikkaan, missä makroa kutsutaan @-alkuisella komennolla. Asian voi itse asiassa tarkistaa @macroexpand-makrolla.

@macroexpand @print_sum_m(1, 2)
Tulostetaan lukujen 1 ja 2 summa eli 3
:(Main.println(3))

Eroavaisuutta alkaa tulemaan, kun makroa käytetään osana muuta ohjelmakoodia. Esimerkiksi tehdään funktio, joka tulostaa kolme kertaa summia 1 ja 2.

function tulosta_summia_m()
    for i=1:3
        @print_sum_m(1, 2)
    end
end

Kun funktion kirjoittaa REPLiin, tulee vastauksena

Tulostetaan lukujen 1 ja 2 summa eli 3

Makro on nyt käännetty osaksi ohjelmakoodia. Kun funktion ajaa:

tulosta_summia_m()
3
3
3

Jos teemme toisen vastaavan funktion, ilman makroa, ja ajamme sen:

function tulosta_summia_f()
    for i=1:3
        print_sum_f(1, 2)
    end
end

tulosta_summia_f()
Tulostetaan lukujen 1 ja 2 summa eli 3
3
Tulostetaan lukujen 1 ja 2 summa eli 3
3
Tulostetaan lukujen 1 ja 2 summa eli 3
3

Näin siksi, ksoka makro ajetaan vain yhden kerran funktion kääntövaiheessa. Vaikka näennäisesti kaikki näyttää suurinpiirtein samalta, niin tarkempi koodin analysointi paljastaa eron:

@code_lowered tulosta_summia_f()
CodeInfo(
1 ─ %1  = 1:3
│         @_2 = Base.iterate(%1)
│   %3  = @_2 === nothing
│   %4  = Base.not_int(%3)
└──       goto #4 if not %4
2 ┄ %6  = @_2
│         i = Core.getfield(%6, 1)
│   %8  = Core.getfield(%6, 2)
│         Main.print_sum_f(1, 2)       # <---
│         @_2 = Base.iterate(%1, %8)
│   %11 = @_2 === nothing
│   %12 = Base.not_int(%11)
└──       goto #4 if not %12
3 ─       goto #2
4 ┄       return
)

Tässä kutsutaan funktiota print_sum_f(1,2), kun taas:

@code_lowered tulosta_summia_m()
CodeInfo(
1 ─ %1  = 1:3
│         @_2 = Base.iterate(%1)
│   %3  = @_2 === nothing
│   %4  = Base.not_int(%3)
└──       goto #4 if not %4
2 ┄ %6  = @_2
│         i = Core.getfield(%6, 1)
│   %8  = Core.getfield(%6, 2)
│         Main.println(3)              # <---
│         @_2 = Base.iterate(%1, %8)
│   %11 = @_2 === nothing
│   %12 = Base.not_int(%11)
└──       goto #4 if not %12
3 ─       goto #2
4 ┄       return
)

Makro on siis tapa esikäsitellä Julian omaa ohjelmakoodia ennen sen kääntämistä. Julia toimii itse itsensä esikäsittelijänä. Makrot eivät ole kaikenkattava ratkaisu kaikkeen, mutta ne voivat toisinaan ratkaista ongelmia tehokkaalla ja kätevällä tavalla, sillä mahdollista laskentaa voidaan siirtää ajonaikaisesta käännösaikaiseen.

Muutamia huomioita. Ensinnäkin, makrot ovat koseptitasolla hieman monimutkaisempia hallita kuin normaalit funktiot, ja siksi niiden kanssa voi tehdä virheitä jotka vievät niiden hyödyn. Tässä esimerkissä laskettiin a + b ennen ohjelman kääntämistä, jolloin voitiin hyödyntää suoraan vastausta. Mikäli makron olisi tehnyt seuraavalla tavalla:

macro print_sum_m2(a, b)
    println("Tulostetaan lukujen $a ja $b summa eli $(a + b)")
    return :(println($a + $b))
end

@print_sum_m2(1, 2)

Koodi kyllä toimii, mutta jos asiaa tarkastelee @macroexpand-komennolla:

@macroexpand @print_sum_m2(1, 2)
Tulostetaan lukujen 1 ja 2 summa eli 3
:(Main.println(1 + 2))

Emme ole esikäsittelijässä siis tehneet oikeastaan mitään hyödyllistä. Potentiaalisesti raskas laskenta on edelleen laskematta, joka tulee käännettävään ohjelmakoodiin sellaisenaan.

Toinen huomio on, että pienet normaalit funktiot kannattaa prefiksata @inline-makrolla, jolloin ne evaluoituvat koodiin sellaisenaan, eivätkä aiheuta overheadia funktiokutsulla. Esimerkiksi:

@inline function tulosta_summia_f2()
    for i=1:3
        print_sum_f(1,2)
    end
end