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

Organizacja kodu

Doświadczenie podpowiada, że bardzo ciężko jest trzymać cały nasz kod w jednym pliku. W tej lekcji przyjrzymy się, jak grupować nasze funkcje w moduły oraz jak za pomocą wyspecjalizowanych map, zwanych strukturami, można efektywnie zorganizować nasz kod.

Moduły

Moduły to najlepsza metoda na zorganizowanie naszego kodu w ramach przestrzeni nazw. Dodatkowo poza grupowaniem funkcji moduły pozwalają na definiowanie funkcji nazwanych oraz prywatnych, które poznaliśmy w poprzedniej lekcji.

Przyjrzyjmy się prostemu przykładowi:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

W Elixirze możliwe jest zagnieżdżanie się modułów, co pozwala na lepszą organizację w naszej przestrzeni nazw:

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

Atrybuty modułów

Atrybuty modułów są najczęściej wykorzystywane do reprezentowania stałych. Przyjrzyjmy się przykładowi:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

Ważne jest, aby pamiętać, że Elixir zawiera zastrzeżone atrybuty. Trzy najpopularniejsze to:

Struktury

Struktury to wyspecjalizowane mapy, które zawierają zbiór kluczy i domyślnych wartości. Struktura musi być zdefiniowana w module, od którego bierze swoją nazwę. Nierzadko struktura jest jedynym elementem zdefiniowanym w module.

By zdefiniować strukturę, używamy słowa kluczowego defstruct wraz z listą asocjacyjną zawierającą nazwy pól i wartości domyślne:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

Stwórzmy zatem kilka struktur:

iex> %Example.User{}
%Example.User<name: "Sean", roles: [], ...>

iex> %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [], ...>

iex> %Example.User{name: "Steve", roles: [:manager]}
%Example.User<name: "Steve", roles: [:manager]>

Struktury można aktualizować tak jak zwykłe mapy:

iex> steve = %Example.User{name: "Steve"}
%Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
%Example.User<name: "Sean", roles: [...], ...>

I najważniejsze. Struktury można dopasowywać tak jak zwykłe mapy:

iex> %{name: "Sean"} = sean
%Example.User<name: "Sean", roles: [...], ...>

Od wersji Elixir 1.8 struktury zawierają niestandardową introspekcję. Aby zrozumieć, co to oznacza i jak mamy z niego korzystać, przyjrzyjmy się naszemu uchwyceniu sean:

iex> inspect(sean)
"%Example.User<name: \"Sean\", roles: [...], ...>"

Wszystkie nasze pola są obecne, co jest w porządku w tym przykładzie, ale co by było, gdybyśmy mieli pole chronione, którego nie chcieliśmy uwzględnić? Nowa funkcja @derive pozwala nam właśnie to osiągnąć! Zaktualizujmy nasz przykład, aby role nie były już uwzględniane w naszych danych wyjściowych:

defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end

Uwaga: moglibyśmy również użyć @derive {Inspect, except: [:roles]}, są one równoważne.

Po zaktualizowaniu naszego modułu przyjrzyjmy się, co dzieje się w iex:

iex> sean = %Example.User{name: "Sean"}
%Example.User<name: "Sean", ...>
iex> inspect(sean)
"%Example.User<name: \"Sean\", ...>"

Pole role zostało pominięte w danych wyjściowych!

Komponenty

Skoro już wiemy jak tworzyć moduły oraz struktury, przyjrzyjmy się jak wykorzystywać je w kodzie z pomocą komponentów. Elixir zapewnia nam wiele różnych sposobów interakcji z innymi modułami. Przyjrzyjmy się, z czego możemy skorzystać.

alias

Pozwala na tworzenie aliasów nazw modułów, co jest bardzo często wykorzystywane w kodzie Elixira:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

# Bez wykorzystania alias

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

Jeśli istnieje konflikt między dwoma aliasami lub po prostu chcemy zmienić alias na zupełnie inną nazwę, możemy użyć opcji :as:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

Można też utworzyć alias do wielu modułów naraz:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

import

Jeżeli zamiast aliasu chcemy dołączyć (zaimportować) funkcje i makra z modułu do naszego kodu, to możemy użyć import:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

Filtrowanie

Domyślnie importowane są wszystkie funkcje i makra, ale możemy odfiltrować tylko część z nich za pomocą opcji :only i :except.

By zaimportować wskazane funkcje i makra, musimy podać nazwę/ilość argumentów jako parametry :only i :except. Zaimportujmy tylko funkcję last/1:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

Jeżeli zaimportujemy wszystkie funkcje poza last/1 i uruchomimy kod z poprzedniego przykładu:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

Poza podaniem pary nazwa/liczba argumentów możemy też użyć dwóch specjalnych atomów :functions i :macros, dzięki którym zaimportujemy odpowiednio tylko funkcje lub tylko makra:

import List, only: :functions
import List, only: :macros

require

Możemy użyć require, aby poinformować Elixir, że zamierzamy używać makr z innego modułu. Niewielka różnica w stosunku do import polega na tym, że pozwala na używanie makr, ale nie funkcji z określonego modułu:

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

Jeżeli spróbujemy wywołać makro, które jeszcze nie zostało załadowane, to otrzymamy błąd.

use

Za pomocą makra use możemy umożliwić innemu modułowi modyfikację naszej aktualnej definicji modułu. Kiedy wywołujemy use w naszym kodzie, w rzeczywistości wywołujemy wywołanie zwrotne __using__/1 zdefiniowane przez dostarczony moduł. Wynik makra __using__/1 staje się częścią definicji naszego modułu. Aby lepiej zrozumieć, jak to działa, spójrzmy na prosty przykład:

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

Stworzyliśmy moduł Hello, który definiuje wywołanie zwrotne __using__/1, wewnątrz którego definiujemy funkcję hello/1. Stwórzmy nowy moduł, abyśmy mogli wypróbować nasz nowy kod:

defmodule Example do
  use Hello
end

Jeśli wypróbujemy nasz kod w IEx, zobaczymy, że hello/1 jest dostępne w module Example:

iex> Example.hello("Sean")
"Hi, Sean"

Widzimy, że dzięki use zostało wykonane wywołanie zwrotne __using__/1 na Hello, co z kolei dodało wynikowy kod do naszego modułu. Teraz gdy zademonstrowaliśmy podstawowy przykład, zaktualizujmy nasz kod, aby zobaczyć, jak __using__/1 obsługuje opcje. Zrobimy to, dodając opcję greeting:

defmodule Hello do
  defmacro __using__(opts) do
    greeting = Keyword.get(opts, :greeting, "Hi")

    quote do
      def hello(name), do: unquote(greeting) <> ", " <> name
    end
  end
end

Zaktualizujmy nasz moduł Example o nowo utworzoną opcję greeting:

defmodule Example do
  use Hello, greeting: "Hola"
end

Jeśli wykonasz kod w IEx, zobaczysz, że powitanie zostało zmienione:

iex> Example.hello("Sean")
"Hola, Sean"

Są to proste przykłady pokazujące, jak działa use, ale jest to niesamowicie potężne narzędzie w Elixir. W miarę jak będziesz uczyć się o Elixirze, wypatruj use, jednym z przykładów, który na pewno zobaczysz, jest use ExUnit.Case, async: true.

Uwaga: quote, alias, use, require to makra związane z metaprogramowaniem.

Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!