„Obiektywne” struktury danych w Elixirze
„Obiektywne”, bo nie „obiektowe”. Przymierzam się do opisania interoperacyjności java-elixir trochę ponad „użyj JInterface”, ale zanim do tego się zabiorę, potrzebujemy mieć kilka dodatkowych narzędzi.
Na początek jednak małe przypomnienie. Już za dwa tygodnie z małym haczykiem tzn. 26 listopada będę mówił o Elixirze na InfoMeet, start o 9:30 w sali C. Będzie zabawnie 🙂
Klasy, obiekty i cała reszta
W językach obiektowych jest prosto. Jeżeli potrzebujemy usystematyzować dane, to definiujemy klasę. Ma ona pola, pola mają wartości. Za czynności odpowiadają metody, które powinny mieć swoje odzwierciedlenie w interfejsach (DDD dla oszczędnych), to tego jest jeszcze element JavaBeans i gotowe. Jak mamy już nasze dane i klasę, to możemy tworzyć obiekty, które reprezentują pewien stan danych. Do tego dochodzą jeszcze kontenery jak na przykład listy, mapy czy sekwencje.
W językach funkcyjnych jest pewien drobny problem…
Podejście funkcyjne
W językach funkcyjnych mamy do dyspozycji m.in. czysto funkcyjne strukturyW, które w uproszczeniu można przyrównać do niezmiennych kolekcji z języków obiektowych (w rzeczywistości najbliżej do tych struktur mają kolekcje z pCollections). Jak jednak odwzorować „klasyczne” obiekty? Zazwyczaj robi się, to poprzez zagnieżdżanie i definiowanie własnych typów.
Na przykładzie Elixira
Osadzanie struktur
Trochę jak w JavaScriptcie 🙂 Czyli nasza złożona struktura to mapa map (o listach, sekwencjach itd. innym razem, ale to nuda jest i tak). Jak wygląda mapa w Elixirze?
Mapy w Elixirze definiuje się w prosty sposób:
Listing 1. Mapa z trzema elementami
iex > mapa = %{:A=> 1, 2=> :b, "C"=> "3"}
%{2 => :b, :A => 1, "C" => "3"}
Jako że Elixir jest słabo typowany, to zarówno klucze, jak i wartości mogą być dowolnego typu. Oczywiście mapy można zagnieżdżać:
Listing 2. Mapa map
iex > mapaMap = %{ "pole" => 1, :pole => mapa}
%{:pole => %{2 => :b, :A => 1, "C" => "3"}, "pole" => 1}
Jest tu jednak pewien „myk”, który będzie bardzo przydatny:
Listing 3. Mapa z kluczami – atomami
iex > mapa = %{:age => 32, :name => "Koziołek"}
%{age: 32, name: "Koziołek"}
Zwróćcie uwagę na, to co stało się z dwukropkami przy nazwach atomów. Zmieniły położenie i z przedrostka stały się przyrostkiem. Technicznie nadal jest, to mapa:
Listing 4. Typ naszej mapy to
iex > i mapa
Term
%{age: 32, name: "Koziołek"}
Data type
Map
Reference modules
Map
I jeszcze jedna zagadka:
Listing 5. Czym jest lista dwuelementowych krotek?
iex > lista = [{:age, 32}, {:name, "Koziołek"}]
[age: 32, name: "Koziołek"]
Lista dwuelementowych krotek, w których pierwszym elementem jest atom, jest tzw. keyword list. Najlepszym odpowiednikiem tego terminu jest lista asocjacyjna. Tak też, to przetłumaczyliśmy w ramach ElixirSchool. W przypadku map jeżeli kluczami są atomy, to Elixir zapewnia spójne z listami asocjacyjnymi API. I jeszcze krótko o typie naszej listy:
Listing 6. Ano jest listą
iex > i lista
Term
[age: 32, name: "Koziołek}]\n\n"]
Data type
List
Description
This is what is referred to as a "keyword list". A keyword list is a list
of two-element tuples where the first element of each tuple is an atom.
Reference modules
Keyword, List
Skoro wiemy już, że można zagnieżdżać mapy i możemy budować w ten sposób struktury podobne do JSON-ów, to rzućmy jeszcze okiem na definiowanie własnych typów.
Własne typy
Zanim zajmiemy się typami mała uwaga, własne typy w Elixirze warto definiować w ramach wydzielonego modułu, ponieważ przy bardziej złożonych strukturach będziemy też korzystać z definicji struktur.
Na początek zdefiniujmy sobie moduł User.Struct (można w nazwie, można zagnieździć, dostęp będzie taki sam), który będzie przechowywać informacje o strukturze użytkownika:
Listing 7. Moduł User.Struct
defmodule User do
defmodule Struct do
defstruct [age: nil, name: nil]
end
end
Teraz dodajmy do naszej struktury typ, konwencja jest taka, że typ nazywamy t i będziemy się do niego odwoływać przez User.Struct.t (plus jedna metoda do testów):
Listing 8. Definicja typu
defmodule User do
defmodule Struct do
defstruct [age: nil, name: nil]
@type t :: %User.Struct{age: integer, name: String.t}
@type t(age, name) :: t :: %User.Struct{age: age, name: name}
end
@spec printInfo(User.Struct.t) :: nil
def printInfo(user) do
IO.puts("User name is #{user.name} and age is #{user.age}")
end
end
Mamy tu tak naprawdę dwa typy. t, który jest „właściwym” zdefiniowanym jako mapa pól dla struktury i t(age, name), który rozszerza t oraz pełni funkcję czysto pomocniczą jeżeli chcemy przedefiniować, któryś z elementów struktury. A jak tego używać?
Listing 9. przykładowe użycie
iex > user = %{:age=> 32, :name=>"Koziołek"}
%{age: 32, name: "Koziołek"}
iex > User.printInfo(user)
User name is Koziołek and age is 32
:ok
Tworzymy sobie naszego użytkownika jako mapę i aplikujemy funkcję. Jeżeli coś popieprzymy, to dostaniemy błąd.
Podsumowanie
Własne typy w Elixirze nie są tak mocne, jak w Javie, ale też nie mamy do czynienia z językiem statycznie typowanym. Warto je jednak wprowadzać i używać, by zachować pewien poziom bezpieczeństwa w naszym kodzie. Miłe…