Elixir to funkcyjny i współbieżny język programowania, który został stworzony w 2012 roku przez José Valim'a (twórca należał min. do zespołu rozwojowego Rails
). Jego głównym celem było połączenie produktywności i elegancji języka Ruby z wydajnością i skalowalnością Erlang
Elixir jest językiem w korzystającym pełnymi garściami z ekosystemu języka Erlang
. Elixir kompiluje się do kodu Erlanga i jest uruchamiany przez maszynę wirtualną BEAM
(ang. Bogdan's Erlang Abstract Machine
) nazywaną także jako Erlang VM.
Jest często on nazywany jako następca Ruby'ego, z którego czerpie wiele w swojej składni, jednak w przeciwieństwie do Ruby ten jest kompilowany, a i także także korzysta z tzw. modelu aktora
(ang. actor model
), który charakteryzuje go wysoką niezawodnością oraz wydajnością.
Dlatego też spisuje się idealnie wszędzie tam, gdzie wymagana jest wydajność oraz niezawodność. Z racji faktu, że wywodzi się z Erlanga to mamy do dyspozycji wszystko to co oferuje Nam Erlang z zakresu OTP
(ang. Online Telecom Protocol
) - o którym będę pisał w następnych artykułach.
Elixir znajduje zastosowanie w wielu różnych dziedzinach, dzięki swojej wydajności i skalowalności:
Język Elixir jest używany przez takie firmy jak:
Na temat programowania funkcjonalnego na przykładzie Elixir'a, oraz po części Haskell'a możecie dowiedzieć się z tego artykułu.
Elixir wymaga Erlang'a, więc najpierw należy zainstalować go, a potem zgodną z Nim wersję Elixira.
O tym jak zainstalować Erlanga, dowiecie się tutaj, a w przypadku Elixira tu.
W przypadku Elixira mamy dwie opcje:
W celu rozpoczęcia projektu, używamy w tym celu narzędzia Mix
, które jest domyślnym narzędziem do zarządzania projektami.
$ mix help
mix # Runs the default task (current: "mix run")
(...)
mix archive # Lists installed archives
(...)
mix xref # Prints cross reference information
iex -S mix # Starts IEx and runs the default task
Projekt tworzymy przy pomocy:
$ mix new foo
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/foo.ex
* creating test
* creating test/test_helper.exs
* creating test/foo_test.exs
Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:
cd foo
mix test
Run "mix help" for more commands.
Pełną, oryginalną dokumentację języka Elixir znajdziecie tu.
Bibliotek do użycia z Naszym kodem, możemy szukać w Hex
'ie: tutaj.
Te następnie dodajemy do mix.exs
:
defp deps do
[
{:plug, "~> 1.1.0"}
]
end
Zależności pobieramy i kompilujemy z pomocą mix deps.get && mix deps.compile
, a kod uruchamiamy z pomocą mix run
lub interaktywnie z pomocą iex -s mix
.
W Elixirze mamy kilka typów danych (w tym miejscu nie będę tłumaczył każdego z nich gdyż uznaję, że zapewne już coś wiesz o programowaniu).
:config
etclist = [1, 2, "3", "four"]
tuple = {:ok, "hi", 89}
map = %{:name => "Alice", "town": "Gdańsk", 1 => "one"}
Można by pomyśleć: że poniższy zapis:
1 = 1 # Wynik => 1
To nic innego jak typowe przypisanie... Nic bardziej mylnego, ponieważ wywołanie poniższego zwróci Nam to:
1 = 2 # Wynik => ** (MatchError) no match of right hand side value: 2
U wielu może to wywołać zdziwienie, lecz biorąc pod uwagę fakt, że dane w Elixirze są niemutowalne tj (niezmienne, z ang. immutable), powyższa sytuacja zaczyna nabierać sensu.
Powyższy przykład jest niczym innym jak dopasowywaniem wzorców (ang. pattern matching), który jest techniką w programowaniu funkcyjnym, która pozwala na sprawdzanie wartości względem określonego wzorca i, jeśli wartość pasuje do wzorca, wyodrębnianie z niej informacji. Jest to elastyczny i wyrazisty sposób podejmowania decyzji na podstawie struktury danych. Innymi słowy można przyrównać to do sprawdzania wartości prawej do lewej i vice versa.
Innym przykładami pattern-matching'u są choćby poniższe.
[1, 2, 3] = [1, 2, 3] # Porównanie prawidłowe
[1, 2, 3] = [1, 2, 4] # Błąd, otrzymamy: MatchError
[head | tail] = [1, 2, 3, 4] # Wynik => head = 1, tail = [2, 3, 4]
[_, second | rest] = [10, 20, 30, 40] # Wynik => second = 20, rest = [30, 40]
{:ok, value} = {:ok, 42} # Wynik => value = 42
{:ok, value} = {:error, "failed"} # Wynik => Otrzymujemy MatchError
%{name: name, age: age} = %{name: "Alice", age: 30} # Wynik => name = "Alice", age = 30
%{name: name} = %{age: 30} # Wynik => Zwraca MatchError
# Nested maps
%{user: %{name: user_name}} = %{user: %{name: "Bob", age: 25}} # Wynik => user_name = "Bob"
Więcej na temat pattern-matching
'u napisałem w tym artykule.
Składnia języka Elixir jest inspirowana oczywiście językiem Ruby
, a i także nie jest nazbyt skomplikowana.
Na rynek wydano kilka naprawdę świetnych książek opisujących ten język w sposób od nowicjusza do profesjonalisty. Z tego powodu nadmiernie też nie będę się nad tym rozpisywał, z racji także faktu, że język ma świetną dokumentację, która może w zupełności i za darmo zastąpić niejedną książkę.
Przypisywanie zmiennych i wzorcowanie zostało opisane we wcześniejszym rozdziale.
Typy sprawdzamy z pomocą funkcji:
is_atom/1
is_bitstring/1
is_boolean/1
is_function/1
is_function/2
is_integer/1
is_float/1
is_binary/1
is_list/1
is_map/1
is_tuple/1
is_nil/1
is_number/1
is_pid/1
is_port/1
is_reference/1
W przypadku funkcji nazwanych
, /1
lub /2
mówią Nam o arności funkcji (ang. arity
) - o tym za chwilę.
Funkcje anonimowe definiujemy tak:
foo = fn arg -> arg * 2 end
IO.inspect foo.(2) # Wynik => 4
Oczywiście należy mieć na uwadze, że każda funkcja może też wywołać inną funkcję, a także fakt, że funkcje anonimowe (lambda) możemy definiować też tak:
foo = &(& * 2)
IO.inspect foo.(2) # Wynik => 4
Funkcje nazwane natomiast wyglądają tak:
defmodule Doo do
def foo(arg) do
arg * 2
end
end
IO.inspect Doo.foo(2) # Wynik => 4
Termin arność funkcji (ang. arity) - referuje do liczby argumentów jakie dana funkcja przyjmuje.
Jest to bardzo ważny koncept w języku Elixir z uwagi na fakt, że ten identyfikuje funkcje nie tylko z jej nazwy, ale także właśnie arności.
Tym samym częstym w języku Elixir jest sytuacja posiadania wielu funkcji o tej samej nazwie. Jest to też ważny element języka w aspekcie rekurencji - będącego w tym przypadku ekwiwalentem dla klasycznych pętli, który w Elixirze nie ma.
cities = [:Warsaw, :Gdansk, :Poznan]
cities |> List.first() # Wynik => :Warsaw
cities_new = cities ++ [:Cracow] # Wynik => [:Warsaw, :Gdansk, :Poznan, :Cracow]
cities_another = [ :Wroclaw | cities_new ] # Wynik => [:Wroclaw, :Warsaw, :Gdansk, :Poznan, :Cracow]
cities_another |> List.last() # Wynik => :Cracow
Enum.map(cities, fn city -> IO.puts "Hello from: " <> to_string(city) end)
# Wynik =>
# Hello from: Warsaw
# Hello from: Gdansk
# Hello from: Poznan
tupl = { :quack, :duck}
tupl |> elem(1) |> to_string() # Wynik => "duck"
To specjalny typ list, w którym każdy element jest krotką z dwoma elementami, którego klucz jest :atomem
. Listy te są posortowane:
# Define a keyword list
opts = [timeout: 3000, retries: 5]
# Add a new key-value pair
opts = Keyword.put(opts, :max_retries, 10)
IO.inspect opts # Wynik => [max_retries: 10, timeout: 3000, retries: 5]
Inaczej słowniki (klucz: wartość):
map = %{a: 1, b: 2, c: 3}
result = map
|> Enum.map(fn {key, value} -> {key, value * 2} end)
|> Enum.into(%{})
IO.inspect(result) # Wynik => %{a: 2, b: 4, c: 6}
Przekazywanie w języku Elixir jest ekwiwalentem zagnieżdżania funkcji znanych z innych języków tj np:
string = "hello world"
uppercase_string = String.upcase(string) # Wynik => "HELLO WORLD"
words_list = String.split(uppercase_string) # Wynik => ["HELLO", "WORLD"]
reversed_words = Enum.map(words_list, &String.reverse/1) # Wynik => ["OLLEH", "DLROW"]
final_string = Enum.join(reversed_words, " ") # Wynik => "OLLEH DLROW"
IO.inspect(final_string) # Wynik => "OLLEH DLROW"
W przypadku Elixira możemy to zrobić tak:
string = "hello world"
final_string = string
|> String.upcase()
|> String.split()
|> Enum.map(&String.reverse/1)
|> Enum.join(" ")
IO.inspect(final_string) # Wynik => "OLLEH DLROW"
Operator |>
służy do przekazywania wyniku jednej funkcji do argumentu następnej funkcji.
Struktury kontrolne (bo tak wypadało by tłumacz z angielskiego control structures) to nic innego jak też inaczej nazywany control flow czyli kontrola przepływów.
Mimo faktu, że w Elixirze jest if
oraz unless
:
something = true
if something do
"This is true"
else
"This is false"
end # Wynik => "This is true"
something = false
unless something do
"This is false"
else
"This is true"
end
# Wynik => "This is false"
Z reguły, częściej stosowane są case
oraz cond
:
x = 666
case x do
0 -> "zero"
1 -> "one"
_ -> "other"
end # Wynik => "other"
cond do
x < 0 -> "negative"
x == 0 -> "zero"
x > 0 -> "positive"
x == 666 -> "hell"
end # Wynik => "hell"
W Elixirze nie uświadczycie znanych z języków obiektowych pętli while
czy until
z Ruby
.
Z racji, że Elixir to język funkcjonalny, natywnym środkiem zastępującym klasyczne pętle, jest rekurencja (ang. recursion):
defmodule Loop do
def countdown(0), do: "Blastoff!"
def countdown(n) do
IO.puts(n)
countdown(n - 1)
end
end
Loop.countdown(3)
Elixir wspiera wyrażenia listowe, które umożliwiają tworzenie nowych list na podstawie istniejących kolekcji:
for n <- 1..4, do: n * n # Wynik => [1, 4, 9, 16]
W Elixirze istnieje możliwość pisania funkcji warunkowych:
defmodule GuardExample do
def check_number(x) when x > 0, do: "Positive"
def check_number(x) when x < 0, do: "Negative"
def check_number(0), do: "Zero"
end
GuardExample.check_number(10) # Wynik => "Positive"
GuardExample.check_number(-5) # Wynik => "Negative"
GuardExample.check_number(0) # Wynik => "Zero"
Moduły w Elixirze służą do grupowania kodu w logiczne jednostki, co ułatwia organizację i zarządzanie kodem. Moduły mogą zawierać funkcje, struktury, a także inne moduły. Dzięki modułom możemy tworzyć bardziej zorganizowane i czytelne aplikacje.
defmodule Math do
# Publiczna funkcja dodająca dwie liczby
def add(a, b) do
a + b
end
# Prywatna funkcja mnożąca dwie liczby
defp multiply(a, b) do
a * b
end
end
IO.puts(Math.add(2, 3)) # Wynik => 5
# Próba wywołania funkcji prywatnej zakończy się błędem
# IO.puts(Math.multiply(2, 3)) # Błąd kompilacji
Moduły mogą być zagnieżdżane, co pozwala na jeszcze lepszą organizację kodu. Zagnieżdżone moduły są definiowane wewnątrz innych modułów.
defmodule Outer do
defmodule Inner do
def greet do
"Hello from the inner module!"
end
end
end
IO.puts(Outer.Inner.greet()) # Wynik => "Hello from the inner module!"
Moduły mogą zawierać atrybuty, które są używane do przechowywania metadanych. Najczęściej używanym atrybutem jest @doc
, który służy do dokumentowania funkcji.
defmodule Documented do
@moduledoc """
Moduł zawierający przykładowe funkcje z dokumentacją.
"""
@doc """
Dodaje dwie liczby.
"""
def add(a, b) do
a + b
end
end
Elixir oferuje kilka sposobów na zarządzanie modułami w kodzie:
defmodule Example do
import Math, only: [add: 2]
use SomeLibrary
alias Some.Long.Module.Name, as: ShortName
def example_function do
add(1, 2)
ShortName.some_function()
end
end
W Elixirze istnieją struktury, będące tak ustrukturyzowanymi mapami
defmodule Engine do
defstruct type: "", horsepower: 0
end
defmodule Car do
defstruct make: "", model: "", year: 0, engine: %Engine{}
end
car = %Car{
make: "Ford",
model: "Mustang",
year: 2021,
engine: %Engine{type: "V8", horsepower: 450}
}
IO.inspect(car) # Wynik => %Car{make: "Ford", model: "Mustang", year: 2021, engine: %Engine{type: "V8", horsepower: 450}}
Składnię jezyka Elixir w bardziej rozwiniętej formie, możecie znaleźć pod tym adresem.