Do you want to pick up from where you left of?
Take me there

Συναρτήσεις

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

Ανώνυμες Συναρτήσεις

Όπως εννοεί το όνομα, μια ανώνυμη συνάρτηση δεν έχει όνομα. Όπως είδαμε στο μάθημα Enum, αυτές συνήθως στέλνονται σε άλλες συναρτήσεις. Για να ορίσουμε μια ανώνυμη συνάρτηση στην Elixir χρειαζόμαστε τις λέξεις κλειδιά fn και end. Μέσα σε αυτές μπορούμε να ορίσουμε οποιοδήποτε αριθμό παραμέτρων και σωμάτων συναρτήσεων χωρισμένα με το ->.

Ας δούμε ένα βασικό παράδειγμα:

iex> sum = fn (a, b) -> a + b end
iex> sum.(2, 3)
5

Η Συντομογραφία &

Η χρήση ανώνυμων συναρτήσεων είναι τόσο κοινή πρακτική στην Elixir που υπάρχει συντομογραφία για αυτές:

iex> sum = &(&1 + &2)
iex> sum.(2, 3)
5

Όπως πιθανότατα θα μαντέψατε ήδη, στην σύντομη έκδοση οι παράμετροι μας είναι διαθέσιμοι σαν &1, &2, &3 και ούτω καθεξής.

Αντιπαραβολή Προτύπων

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

Η Elixir χρησιμοποιεί αντιπαραβολή προτύπων για να αναγνωρίσει το πρώτο σετ παραμέτρων που ταιριάζουν και καλεί το αντίστοιχο σώμα:

iex> handle_result = fn
...>   {:ok, result} -> IO.puts "Handling result..."
...>   {:ok, _} -> IO.puts "This would be never run as previous will be matched beforehand."
...>   {:error} -> IO.puts "An error has occurred!"
...> end

iex> some_result = 1
1
iex> handle_result.({:ok, some_result})
Handling result...
:ok
iex> handle_result.({:error})
An error has occurred!

Ονομασμένες Συναρτήσεις

Μπορούμε να ορίσουμε συναρτήσεις με ονόματα ώστε να μπορούμε αργότερα με ευκολία να αναφερθούμε σε αυτές. Οι ονομασμένες συναρτήσεις ορίζονται μέσα σε μια ενότητα (module) χρησιμοποιώντας την λέξη κλειδί def. Θα μάθουμε περισσότερα για τις ενότητες στα επόμενα μαθήματα, για την ώρα θα εστιάσουμε μόνο στις ονομασμένες συναρτήσεις.

Οι συναρτήσεις που ορίζονται μέσα σε μια ενότητα είναι διαθέσιμες για χρήση σε άλλες ενότητες. Αυτή είναι μια εξαιρετικά σημαντική δομή στην Elixir:

defmodule Greeter do
  def hello(name) do
    "Hello, " <> name
  end
end

iex> Greeter.hello("Sean")
"Hello, Sean"

Αν το σώμα της συνάρτησης αποτελείται από μόνο μία γραμμή, μπορούμε να το συντομεύσουμε περαιτέρω με την do::

defmodule Greeter do
  def hello(name), do: "Hello, " <> name
end

Οπλισμένοι με τις γνώσεις μας στην αντιπαραβολή προτύπων, ας δούμε το θέμα της αναδρομής χρησιμοποιώντας ονομασμένες συναρτήσεις:

defmodule Length do
  def of([]), do: 0
  def of([_ | tail]), do: 1 + of(tail)
end

iex> Length.of []
0
iex> Length.of [1, 2, 3]
3

Ονομασία Συναρτήσεων και Τάξη

Αναφέραμε νωρίτερα ότι οι συναρτήσεις ονομάζονται από το συνδυασμό του δωσμένου ονόματος και την τάξη (αριθμός των ορισμάτων). Αυτό σημαίνει ότι μπορείτε να κάνετε πράγματα όπως αυτό:

defmodule Greeter2 do
  def hello(), do: "Hello, anonymous person!"   # hello/0
  def hello(name), do: "Hello, " <> name        # hello/1
  def hello(name1, name2), do: "Hello, #{name1} and #{name2}"
                                                # hello/2
end

iex> Greeter2.hello()
"Hello, anonymous person!"
iex> Greeter2.hello("Fred")
"Hello, Fred"
iex> Greeter2.hello("Fred", "Jane")
"Hello, Fred and Jane"

Στο παραπάνω παράδειγμα έχουμε παραθέσει τα ονόματα συναρτήσεων σε σχόλια. Η πρώτη υλοποίηση δεν δέχεται ορίσματα, έτσι είναι γνωστή σαν hello/0. Η δεύτερη δέχεται ένα όρισμα, έτσι είναι γνωστή σαν hello/1, και ούτω καθεξής. Αντίθετα με την υπερφόρτωση συναρτήσεων σε κάποιες άλλες γλώσσες, αυτές τις σκεφτόμαστε σαν διαφορετικές συναρτήσεις μεταξύ τους. (Η αντιπαραβολή προτύπων, που αναφέραμε λίγο πριν, εφαρμόζεται μόνο όταν παρέχονται πολλαπλοί ορισμοί για ορισμούς συναρτήσεων με τον ίδιο αριθμό ορισμάτων.)

Συναρτήσεις και Αντιπαραβολή Προτύπων

Στο παρασκήνιο, οι συναρτήσεις αντιπαραβάλουν τα ορίσματα με τα οποία καλούνται.

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

defmodule Greeter1 do
  def hello(%{name: person_name}) do
    IO.puts "Hello, " <> person_name
  end
end

Τώρα ας πούμε πως έχουμε ένα χάρτη που περιγράφει ένα άτομο που λέγεται Fred:

iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Αυτά είναι τα αποτελέσματα που θα πάρουμε αν καλέσουμε την Greeter1.hello/1 με τον χάρτη fred:

# call with entire map
...> Greeter1.hello(fred)
"Hello, Fred"

Τι συμβαίνει όταν καλέσουμε τη συνάρτηση με ένα χάρτη που δεν περιέχει το κλειδί :name;

# call without the key we need returns an error
...> Greeter1.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter1.hello/1

    The following arguments were given to Greeter1.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:12: Greeter1.hello/1

Ο λόγος για αυτή τη συμπεριφορά είναι ότι η Elixir αντιπαραβάλει τα ορίσματα με τα οποία η συνάρτηση καλείται απέναντι στην τάξη με την οποία η συνάρτηση ορίζεται.

Ας σκεφτούμε πως δείχνουν τα δεδομένα όταν φτάνουν στην Greeter1.hello/1:

# incoming map
iex> fred = %{
...> name: "Fred",
...> age: "95",
...> favorite_color: "Taupe"
...> }

Η Greeter1.hello/1 περιμένει ένα όρισμα σαν αυτό:

%{name: person_name}

Στην Greeter1.hello/1, ο χάρτης που περνάμε (fred) αντιπαραβάλεται με το όρισμά μας (%{name: person_name}):

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

Βρίσκει ότι υπάρχει κλειδί που αντιστοιχεί στο name στον εισερχόμενο χάρτη. Έχουμε ταίριασμα! Και σαν αποτέλεσμα απο αυτό το επιτυχές ταίριασμα, η τιμή του κλειδιού :name στο χάρτη στα δεξιά (π.χ. ο χάρτης fred) ορίζεται στη μεταβλητή στα αριστερά (person_name).

Τώρα, τι θα γινόταν αν θέλαμε να ορίσουμε το όνομα του Fred στην μεταβλητή person_name αλλά επίσης θέλαμε να διατηρήσουμε τον πλήρη χάρτη του ατόμου; Ας πούμε ότι θέλουμε να τρέξουμε την IO.inspect(fred) αφού τον χαιρετίσουμε. Σε αυτό το σημείο, επειδή αντιπαραβάλαμε μόνο το κλειδί :name του χάρτη, και έτσι καταχωρήσαμε μόνο την τιμή αυτού του κλειδιού σε μεταβλητή, η συνάρτηση δεν έχει γνώση από το υπόλοιπο του Fred.

Για να τον διατηρήσουμε, πρέπει να καταχωρήσουμε τον πλήρη χάρτη στην δική του μεταβλητή ώστε να μπορέσουμε να τον χρησιμοποιήσουμε.

Ας ξεκινήσουμε μια νέα συνάρτηση:

defmodule Greeter2 do
  def hello(%{name: person_name} = person) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

Θυμηθείτε ότι η Elixir θα αντιπαραβάλει το όρισμα καθώς έρχεται στη συνάρτηση. Έτσι σε αυτή την περίπτωση, κάθε πλευρά θα αντιπαραβληθεί απέναντι στο εισερχόμενο όρισμα και θα οριστεί σε οτιδήποτε ταιριάζει. Ας πάρουμε τη δεξιά πλευρά πρώτα:

person = %{name: "Fred", age: "95", favorite_color: "Taupe"}

Τώρα, η μεταβλητή person έχει οριστεί στον πλήρη χάρτη fred. Πάμε στην επόμενη αντιπαραβολή:

%{name: person_name} = %{name: "Fred", age: "95", favorite_color: "Taupe"}

Τώρα, αυτή είναι ίδια με την αρχική Greeter1 συνάρτηση όπου ταιριάζουμε το χάρτη και κρατάμε μόνο το όνομα του Fred. Αυτό που πετυχαίνουμε είναι δύο μεταβλητές που μπορούμε να χρησιμοποιήσουμε αντί για μία:

  1. person, που αντιστοιχεί σε %{name: "Fred", age: "95", favorite_color: "Taupe"}
  2. person_name, που αντιστοιχεί σε "Fred"

Έτσι όταν τώρα καλέσουμε την Greeter2.hello/1, μπορούμε να χρησιμοποιήσουμε όλες τις πληροφορίες του Fred:

# call with entire person
...> Greeter2.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}
# call with only the name key
...> Greeter2.hello(%{name: "Fred"})
"Hello, Fred"
%{name: "Fred"}
# call without the name key
...> Greeter2.hello(%{age: "95", favorite_color: "Taupe"})
** (FunctionClauseError) no function clause matching in Greeter2.hello/1

    The following arguments were given to Greeter2.hello/1:

        # 1
        %{age: "95", favorite_color: "Taupe"}

    iex:15: Greeter2.hello/1

Έτσι είδαμε ότι η Elixir αντιπαραβάλει σε πολλαπλά επίπεδα επειδή κάθε όρισμα αντιπαραβάλεται με τα εισερχόμενα δεδομένα ανεξάρτητα, αφήνοντάς μας με τις μεταβλητές που μπορούμε στη συνέχεια να χρησιμοποιήσουμε μέσα στη συνάρτησή μας.

Αν αλλάξουμε τη σειρά των %{name: person_name} και person στη λίστα, θα πάρουμε το ίδιο αποτέλεσμα επειδή κάθε ένα αντιπαραβάλεται με το Fred ξεχωριστά.

Αλλάζοντας τη σειρά μεταβλητής και χάρτη:

defmodule Greeter3 do
  def hello(person = %{name: person_name}) do
    IO.puts "Hello, " <> person_name
    IO.inspect person
  end
end

Και την καλούμε με τα ίδια δεδομένα που χρησιμοποιήσαμε στην Greeter2.hello/1:

# call with same old Fred
...> Greeter3.hello(fred)
"Hello, Fred"
%{age: "95", favorite_color: "Taupe", name: "Fred"}

Θυμηθείτε ότι παρόλο που το %{name: person_name} = person δείχνει σαν να γίνεται αντιπαραβολή του %{name: person_name} με την μεταβλητή person, στην πραγματικότητα κάθε μια από αυτές αντιπαραβάλονται στo εισερχόμενo όρισμα.

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

Ιδιωτικές Συναρτήσεις

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

defmodule Greeter do
  def hello(name), do: phrase() <> name
  defp phrase, do: "Hello, "
end

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.phrase
** (UndefinedFunctionError) function Greeter.phrase/0 is undefined or private
    Greeter.phrase()

Προστάτες

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

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

defmodule Greeter do
  def hello(names) when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names)
  end

  def hello(name) when is_binary(name) do
    phrase() <> name
  end

  defp phrase, do: "Hello, "
end

iex> Greeter.hello ["Sean", "Steve"]
"Hello, Sean, Steve"

Προκαθορισμένες Παράμετροι

Αν θέλουμε μια προκαθορισμένη τιμή για μια παράμετρο χρησιμοποιούμε το συντακτικό παράμετρος \\ τιμή:

defmodule Greeter do
  def hello(name, language_code \\ "en") do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("gr"), do: "Γειά σου, "
end

iex> Greeter.hello("Sean", "en")
"Hello, Sean"

iex> Greeter.hello("Sean")
"Hello, Sean"

iex> Greeter.hello("Sean", "gr")
"Γειά σου, Sean"

Όταν συνδυάσουμε το παράδειγμα προστάτη με τις προκαθορισμένες παραμέτρους, συναντάμε ένα πρόβλημα. Ας δούμε πως θα έμοιαζε κάτι τέτοιο:

defmodule Greeter do
  def hello(names, language_code \\ "en") when is_list(names) do
    names = Enum.join(names, ", ")
    
    hello(names, language_code)
  end

  def hello(name, language_code \\ "en") when is_binary(name) do
    phrase(language_code) <> name
  end

  defp phrase("en"), do: "Hello, "
  defp phrase("gr"), do: "Γειά σου, "
end

** (CompileError) iex:8: def hello/2 defines defaults multiple times. Elixir allows defaults to be declared once per definition.
Instead of:

    def foo(:first_clause, b \\ :default) do ... end
    def foo(:second_clause, b \\ :default) do ... end

one should write:

    def foo(a, b \\ :default)
    def foo(:first_clause, b) do ... end
    def foo(:second_clause, b) do ... end
    ```

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

defmodule Greeter do def hello(names, language_code \ “en”)

def hello(names, language_code) when is_list(names) do

names = Enum.join(names, ", ")

hello(names, language_code)

end

def hello(name, language_code) when is_binary(name) do

phrase(language_code) <> name

end

defp phrase(“en”), do: “Hello, “ defp phrase(“el”), do: “Γειά σου, “ end

iex> Greeter.hello [“Sean”, “Steve”] “Hello, Sean, Steve”

iex> Greeter.hello [“Sean”, “Steve”], “el” “Γειά σου, Sean, Steve”

Έπιασες λάθος ή θέλεις να συνεισφέρεις στο μάθημα; Επεξεργαστείτε αυτό το μάθημα στο GitHub!