Fork me on GitHub

Μεταπρογραμματισμός

Ο Μεταπρογραμματισμός είναι η διαδικασία της χρήσης κώδικα που γράφει κώδικα. Στην Elixir αυτό μας δίνει τη δυνατότητα να επεκτείνουμε τη γλώσσα ώστε να καλύπτει τις ανάγκες μας και να αλλάζει τον κώδικά μας δυναμικά. Θα ξεκινήσουμε βλέποντας πως η Elixir παρουσιάζεται κάτω από το καπώ, πως να την αλλάξουμε, και τέλος πως να χρησιμοποιήσουμε αυτή τη γνώση για να την επεκτείνουμε.

Προειδοποίηση: ο Μεταπρογραμματισμός είναι δύσκολος και θα πρέπει να χρησιμοποιείται μόνο όταν είναι απολύτως απαραίτητος. Η υπερβολική χρήση είναι σχεδόν σίγουρο ότι θα οδηγήσει σε περίπλοκο κώδικα που θα είναι δύσκολο να καταλάβουμε και διορθώσουμε.

Πίνακας περιεχομένων

Παράθεση (Quote)

Το πρώτο βήμα στο μεταπρογραμματισμό είναι η κατανόηση του πως παρουσιάζονται οι εκφράσεις. Στην Elixir, το αφηρημένο συντακτικό δέντρο (abstract syntax tree - AST), η εσωτερική παρουσίαση του κώδικά μας, απαρτίζεται από τούπλες. Αυτές οι τούπλες περιέχουν τρία μέρη: το όνομα της συνάρτησης, τα μεταδεδομένα, και τα ορίσματα της συνάρτησης.

Για να δούμε αυτές τις εσωτερικές δομές, η Elixir μας παρέχει τη συνάρτηση της quote/2. Με τη χρήση της quote/2 μπορούμε να μετατρέψουμε τον κώδικα Elixir στη βασική του παρουσίαση:

iex> quote do: 42
42
iex> quote do: "Γειά"
"Γειά"
iex> quote do: :world
:world
iex> quote do: 1 + 2
{:+, [context: Elixir, import: Kernel], [1, 2]}
iex> quote do: if value, do: "Αλήθεια", else: "Ψέματα"
{:if, [context: Elixir, import: Kernel],
 [{:value, [], Elixir}, [do: "Αλήθεια", else: "Ψέματα"]]}

Παρατηρείτε πως τα πρώτα τρία δεν επιστρέφουν τούπλες; Υπάρχουν πέντε κυριολεκτικά που επιστρέφουν τον εαυτό τους όταν παρατίθενται:

iex> :atom
:atom
iex> "string"
"string"
iex> 1 # Όλοι οι αριθμοί
1
iex> [1, 2] # Λίστες
[1, 2]
iex> {"hello", :world} # τούπλες 2 στοιχείων
{"hello", :world}

Αποπαράθεση (Unquote)

Τώρα που μπορούμε να πάρουμε την εσωτερική δομή του κώδικά μας, πως την μετατρέπουμε; Για να εισάγουμε νέο κώδικα ή τιμές χρησιμοποιούμε την unquote/1. Όταν αποπαρατίθεται μια έκφραση, αυτή θα εκτιμηθεί και εισηχθεί στο AST. Για να επιδείξουμε την unquote/1, ας δούμε μερικά παραδείγματα:

iex> denominator = 2
2
iex> quote do: divide(42, denominator)
{:divide, [], [42, {:denominator, [], Elixir}]}
iex> quote do: divide(42, unquote(denominator))
{:divide, [], [42, 2]}

Στο πρώτο παράδειγμα, η μεταβλητή denominator παρατίθεται και έτσι το παραγόμενο AST περιλαμβάνει μια τούπλα για την πρόσβαση στη μεταβλητή. Αντίθετα, στο παράδειγμα της unquote/1, ο παραγόμενος κώδικας περιλαμβάνει την τιμή της denominator.

Μακροεντολές

Όταν κατανοήσουμε τις quote/2 και unquote/1, είμαστε έτοιμοι να ασχοληθούμε με τις μακροεντολές. Είναι σημαντικό να θυμόμαστε ότι οι μακροεντολές, όπως ο μεταπρογραμματισμός, πρέπει να χρησιμοποιούνται με οικονομία.

Εν συντομία, οι μακροεντολές είναι ειδικές συναρτήσεις σχεδιασμένες να επιστρέφουν μια παρατιθέμενη έκφραση που θα εισηχθεί στον κώδικα της εφαρμογής μας. Φανταστείτε ότι η μακροεντολή αντικαθιστάται με την παρατιθέμενη έκφραση αντί να κληθεί σαν συνάρτηση. Με τις μακροεντολές, έχουμε ότι είναι απαραίτητο για να επεκτείνουμε την Elixir και να προσθέσουμε δυναμικά κώδικα στις εφαρμογές μας.

Ξεκινάμε ορίζοντας μια μακροεντολή με τη χρήση της defmacro/2, η οποία, όπως πολλά στην Elixir, είναι και η ίδια μακροεντολή (κρατήστε το αυτό). Σαν παράδειγμα θα υλοποιήσουμε την unless σαν μακροεντολή. Θυμηθείτε ότι οι μακροεντολές μας πρέπει να επιστρέφουν μια παρατιθέμενη έκφραση:

defmodule OurMacro do
  defmacro unless(expr, do: block) do
    quote do
      if !unquote(expr), do: unquote(block)
    end
  end
end

Ας απαιτήσουμε την ενότητά μας και ας πάμε την μακροεντολή μια βόλτα:

iex> require OurMacro
nil
iex> OurMacro.unless true, do: "Hi"
nil
iex> OurMacro.unless false, do: "Hi"
"Hi"

Επειδή οι μακροεντολές αντικαθιστούν κώδικα στην εφαρμογή μας, μπορούμε να χειριστούμε το πότε και το τι συντάσσεται. Ένα παράδειγμα αυτού μπορεί να βρεθεί στην ενότητα Logger. Όταν η καταγραφή είναι απενεργοποιημένη δεν εισάγεται κώδικας και η παραγόμενη εφαρμογή δεν περιέχει αναφορές ή κλήσεις συναρτήσεων καταγραφής. Αυτό είναι διαφορετικό από άλλες γλώσσες όπου υπάρχει ακόμα το βάρος μιας κλήσης συνάρτησης ακόμα και όταν η υλοποίηση δεν τη χρησιμοποιεί.

Για να κάνουμε μια επίδειξη αυτού, θα δημιουργήσουμε έναν απλό καταγραφέα ο οποίος μπορεί είτε να ενεργοποιηθεί ή να απενεργοποιηθεί:

defmodule Logger do
  defmacro log(msg) do
    if Application.get_env(:logger, :enabled) do
      quote do
        IO.puts("Καταγεγραμμένο Μήνυμα: #{unquote(msg)}")
      end
    end
  end
end

defmodule Example do
  require Logger

  def test do
    Logger.log("Αυτό είναι ένα μήνυμα καταγραφής!")
  end
end

Με την καταγραφή ενεργοποιημένη, η συνάρτησή test θα παράγει κώδικα που θα δείχνει όπως παρακάτω:

def test do
  IO.puts("Καταγεγραμμένο Μήνυμα: #{"Αυτό είναι ένα μήνυμα καταγραφής"}")
end

Αλλά αν απενεργοποιήσουμε την καταγραφή, ο παραγόμενος κώδικας θα είναι:

def test do
end

Απασφαλμάτωση

Ωραία, τώρα ξέρουμε πως να χρησιμοποιήσουμε τις quote/2, unquote/1 και πως να γράφουμε μακροεντολές. Τι γίνεται όμως αν έχουμε ένα μεγάλο μέρος παραθετιμένου κώδικα και θέλουμε να τον κατανοήσουμε; Σε αυτή την περίπτωση, μπορείτε να χρησιμοποιήσετε την Macro.to_string/2. Ρίξτε μια ματιά στο παράδειγμα:

iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
"foo.bar(1, 2, 3)"

Και όταν θέλετε να δείτε τον κώδικα που παράγεται από τις μακροεντολές μπορείτε να τις συνδυάσετε με τις Macro.expand/2 και Macro.expand_once/2, οι οποίες αναπτύσσουν τις μακροεντολές στον δοσμένο παρατιθέμενο κώδικα. Η πρώτη μπορεί να τον αναπτύξει πολλές φορές, ενώ η δεύτερη μόνο μία. Για παράδειγμα, ας αλλάξουμε το παράδειγμα της unless από τον προηγούμενο τομέα:

defmodule OurMacro do
  defmacro unless(expr, do: block) do
    quote do
      if !unquote(expr), do: unquote(block)
    end
  end
end

require OurMacro
quoted = quote do
  OurMacro.unless true, do: "Γειά"
end
iex> quoted |> Macro.expand_once(__ENV__) |> Macro.to_string |> IO.puts
if(!true) do
  "Γειά"
end

Αλλά αν τρέξουμε τον ίδιο κώδικα με την Macro.expand/2, γίνεται ενδιαφέρον:

iex> quoted |> Macro.expand(__ENV__) |> Macro.to_string |> IO.puts
case(!true) do
  x when x in [false, nil] ->
    nil
  _ ->
    "Γειά"
end

Όπως θα θυμάστε, αναφέραμε ότι η if είναι μια μακροεντολή στην Elixir, εδώ μπορούμε να την δούμε αναπτυγμένη στην βασική της δήλωση case.

Ιδιωτικές Μακροεντολές

Παρόλο που δεν είναι συνηθισμένο, η Elixir υποστηρίζει ιδιοτικές μακροεντολές. Μια ιδιωτική μακροεντολή ορίζεται με την defmacrop και μπορεί μόνο να κληθεί από την ενότητα στην οποία ορίζεται. Οι ιδιοτικές μακροεντολές πρέπει να ορίζονται πριν από τον κώδικα που τις καλεί.

Υγιεινή Μακροεντολών

Ο τρόπος που αλληλεπιδρούν οι μακροεντολές με τα συμφραζόμενα αυτού που την καλεί όταν αναπτυχθεί, είναι γνωστός και ως υγειεινή μακροεντολών. Οι μακροεντολές στην Elixir είναι υγιεινές από φυσικού τους και δεν θα συγκρουστούν με το περιβάλλον μας:

defmodule Example do
  defmacro hygienic do
    quote do: val = -1
  end
end

iex> require Example
nil
iex> val = 42
42
iex> Example.hygienic
-1
iex> val
42

Τι θα γινόταν όμως αν θέλαμε να χειριστούμε την τιμή της val; Για να χαρακτηρίσουμε μια μεταβλητή ως ανθυγιεινή μπορούμε να χρησιμοποιήσουμε την var!/2. Ας αναβαθμίσουμε το παράδειγμά μας με μια άλλη μακροεντολή που κάνει χρήση της var!/2:

defmodule Example do
  defmacro hygienic do
    quote do: val = -1
  end

  defmacro unhygienic do
    quote do: var!(val) = -1
  end
end

Ας συγκρίνουμε τώρα πως αλληλεπιδρούν με το περιβάλλον μας:

iex> require Example
nil
iex> val = 42
42
iex> Example.hygienic
-1
iex> val
42
iex> Example.unhygienic
-1
iex> val
-1

Ενσωματώνοντας την var!/2 στην μακροεντολή μας, χειριστίκαμε την τιμή της val χωρίς να την περάσουμε στη μακροεντολή. Η χρήση των ανθυγιεινών μακροεντολών θα πρέπει να περιορίζεται. Ενσωματώνοντας την var!/2 αυξάνουμε το ρίσκο μιας σύγκρουσης μεταβλητών.

Δέσιμο

Καλύψαμε ήδη τη χρησιμότητα της unquote/1, αλλά πάρχει ένας άλλος τρόπος να εισάγουμε τιμές στον κώδικά μας: το δέσιμο. Με το δέσιμο μεταβλητών είμαστε σε θέση να συμπεριλάβουμε πολλαπλές μεταβλητές στις μακροεντολές μας και να βεβαιωθούμε ότι αποπαρατίθενται μόνο μια φορά, αποφεύγοντας τις ατυχείς επανεκτιμήσεις. Για να χρησιμοποιήσουμε δεμένες μεταβλητές θα πρέπει να περάσουμε μια λίστα λέξεων κλειδί στην επιλογή bind_quoted της quote/2.

Για να δούμε το όφελος της bind_quote και να επιδείξουμε το θέμα με την επανεκτίμηση ας χρησιμοποιήσουμε ένα παράδειγμα. Μπορούμε να αρχίσουμε με τη δημιουργία μιας μακροεντολής που απλά παραθέτει την έκφραση δύο φορές:

defmodule Example do
  defmacro double_puts(expr) do
    quote do
      IO.puts unquote(expr)
      IO.puts unquote(expr)
    end
  end
end

Θα δοκιμάσουμε τη νέα μας μακροεντολή περνώντας της την τρέχουσα ώρα συστήματος. Θα πρέπει να δούμε την έξοδο δύο φορές:

iex> Example.double_puts(:os.system_time)
1450475941851668000
1450475941851733000

Οι ώρες είναι διαφορετικές! Τι συνέβη; Η χρήση της unquote/1 στην ίδια έκφραση πολλαπλές φορές έχει σαν αποτέλεσμα την επανεκτίμησή της και αυτό μπορεί να έχει απρόσκοπτες συνέπειες. Ας αναβαθμίσουμε το παράδειγμα ώστε να χρησιμοποεί την bind_quoted και ας δούμε τι παίρνουμε:

defmodule Example do
  defmacro double_puts(expr) do
    quote bind_quoted: [expr: expr] do
      IO.puts expr
      IO.puts expr
    end
  end
end

iex> require Example
nil
iex> Example.double_puts(:os.system_time)
1450476083466500000
1450476083466500000

Με την bind_quoted παίρνουμε το προσδωκόμενο αποτέλεσμα: την ίδια ώρα εκτυπωμένη δις.

Τώρα που καλύψαμε τις quote/2, unquote/1, και την defmacro/2 έχουμε όλα τα εργαλεία που είναι απαραίτητα για να επεκτείνουμε την Elixir ώστε να καλύπτει τις ανάγκες μας.


Share This Page