„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…