Spis treści
- Spis treści
- Wstęp
- Programowanie funkcjonalne
- Instalacja
- Praca z kodem
- Biblioteki
- Typy danych
- Zmienne i dopasowywanie wzorców
- Składnia
- Pozostałe elementy składni
- Referencje
Wstęp
Czym jest Elixir?
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.
Zastosowanie języka Elixir
Elixir znajduje zastosowanie w wielu różnych dziedzinach, dzięki swojej wydajności i skalowalności:
-
Aplikacje Webowe: Framework Phoenix, zbudowany na Elixirze, jest często używany do tworzenia skalowalnych aplikacji webowych. Dzięki wsparciu dla WebSockets i LiveView, Phoenix umożliwia tworzenie interaktywnych aplikacji w czasie rzeczywistym.
-
Systemy Rozproszone: Elixir, działający na maszynie wirtualnej Erlanga, jest idealny do budowy systemów rozproszonych. Jego model współbieżności pozwala na łatwe zarządzanie wieloma procesami, co jest kluczowe w systemach wymagających wysokiej dostępności.
-
IoT (Internet of Things): Elixir jest używany w projektach IoT, gdzie wymagana jest obsługa wielu urządzeń jednocześnie. Jego zdolność do zarządzania wieloma równoczesnymi połączeniami sprawia, że jest idealnym wyborem do takich zastosowań.
-
Telekomunikacja: Dzięki swojej niezawodności i wydajności, Elixir jest używany w branży telekomunikacyjnej do budowy systemów, które muszą obsługiwać ogromne ilości danych i połączeń w czasie rzeczywistym.
-
Finanse: W sektorze finansowym, gdzie niezawodność i szybkość są kluczowe, Elixir jest używany do budowy systemów transakcyjnych i analitycznych.
-
Gry: Elixir znajduje również zastosowanie w branży gier, szczególnie w tworzeniu serwerów gier, które muszą obsługiwać dużą liczbę graczy jednocześnie.
Język Elixir jest używany przez takie firmy jak:
- Discord: Popularna platforma komunikacyjna używa Elixira do obsługi milionów równoczesnych połączeń.
- Pinterest: Wykorzystuje Elixira do zarządzania swoją infrastrukturą back-end’ową.
- Bleacher Report: Używa Elixira do obsługi swoich aplikacji mobilnych i webowych.
Programowanie funkcjonalne
Na temat programowania funkcjonalnego na przykładzie Elixir’a, oraz po części Haskell’a możecie dowiedzieć się z tego artykułu.
Instalacja
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.
Praca z kodem
W przypadku Elixira mamy dwie opcje:
- Interaktywny Shell Elixira (IEx)
- Klasyczna praca z kodem
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 app.config # Configures all registered apps
mix app.start # Starts all registered apps
mix app.tree # Prints the application tree
mix archive # Lists installed archives
mix archive.build # Archives this project into a .ez file
mix archive.install # Installs an archive locally
mix archive.uninstall # Uninstalls archives
mix clean # Deletes generated application files
mix cmd # Executes the given command
mix compile # Compiles source files
mix deps # Lists dependencies and their status
mix deps.clean # Deletes the given dependencies' files
mix deps.compile # Compiles dependencies
mix deps.get # Gets all out of date dependencies
mix deps.tree # Prints the dependency tree
mix deps.unlock # Unlocks the given dependencies
mix deps.update # Updates the given dependencies
mix do # Executes the tasks separated by plus
mix escript # Lists installed escripts
mix escript.build # Builds an escript for the project
mix escript.install # Installs an escript locally
mix escript.uninstall # Uninstalls escripts
mix eval # Evaluates the given code
mix format # Formats the given files/patterns
mix help # Prints help information for tasks
mix hex # Prints Hex help information
mix hex.audit # Shows retired Hex deps for the current project
mix hex.build # Builds a new package version locally
mix hex.config # Reads, updates or deletes local Hex config
mix hex.docs # Fetches or opens documentation of a package
mix hex.info # Prints Hex information
mix hex.organization # Manages Hex.pm organizations
mix hex.outdated # Shows outdated Hex deps for the current project
mix hex.owner # Manages Hex package ownership
mix hex.package # Fetches or diffs packages
mix hex.publish # Publishes a new package version
mix hex.registry # Manages local Hex registries
mix hex.repo # Manages Hex repositories
mix hex.retire # Retires a package version
mix hex.search # Searches for package names
mix hex.sponsor # Show Hex packages accepting sponsorships
mix hex.user # Manages your Hex user account
mix loadconfig # Loads and persists the given configuration
mix local # Lists tasks installed locally via archives
mix local.hex # Installs Hex locally
mix local.phx # Updates the Phoenix project generator locally
mix local.public_keys # Manages public keys
mix local.rebar # Installs Rebar locally
mix new # Creates a new Elixir project
mix phx.new # Creates a new Phoenix v1.7.12 application
mix phx.new.ecto # Creates a new Ecto project within an umbrella project
mix phx.new.web # Creates a new Phoenix web project within an umbrella project
mix profile.cprof # Profiles the given file or expression with cprof
mix profile.eprof # Profiles the given file or expression with eprof
mix profile.fprof # Profiles the given file or expression with fprof
mix profile.tprof # Profiles the given file or expression with tprof
mix release # Assembles a self-contained release
mix release.init # Generates sample files for releases
mix run # Runs the current application
mix test # Runs a project's tests
mix test.coverage # Build report from exported test coverage
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.
Biblioteki
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
.
Typy danych
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).
- Liczby całkowite (ang. integer)
- Liczby zmiennoprzecinkowe (ang. float)
- Wartości logiczne (ang. boolean)
- Atomy (ang. atoms) - Innymi słowy stałe, który nazwa jest równocześnie ich wartością:
:config
etc - Ciągi znaków (ang. string)
- Listy (ang. lists) - Dynamiczne, dowolnej długości z elementami dowolnego typu:
list = [1, 2, "3", "four"]
- Krotki (ang. tuples) - Struktury o stałej długości, mogące przechowywać elementy dowolnego typu np.
tuple = {:ok, "hi", 89}
- Mapy (ang. maps) - Znane w innych językach jako słowniki (para: klucz), które definiujemy z pomocą % np.
map = %{:name => "Alice", "town": "Gdańsk", 1 => "one"}
Zmienne i dopasowywanie wzorców
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.
Porównywanie na listach
[1, 2, 3] = [1, 2, 3] # Porównanie prawidłowe
[1, 2, 3] = [1, 2, 4] # Błąd, otrzymamy: MatchError
Destrukcja list (tak jakby)
[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]
Porównywanie krotek
{:ok, value} = {:ok, 42} # Wynik => value = 42
{:ok, value} = {:error, "failed"} # Wynik => Otrzymujemy MatchError
Porównywanie map
%{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
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
Przypisywanie zmiennych i wzorcowanie zostało opisane we wcześniejszym rozdziale.
Sprawdzanie typów
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
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
Funkcje nazwane natomiast wyglądają tak:
defmodule Doo do
def foo(arg) do
arg * 2
end
end
IO.inspect Doo.foo(2) # Wynik => 4
Arność funkcji
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.
Kolekcje
Listy
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
Krotki
tupl = { :quack, :duck}
tupl |> elem(1) |> to_string() # Wynik => "duck"
Listy kluczy
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]
Mapy
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
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
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"
Pętle czy rekurencja?
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)
Wyrażenia listowe
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]
Guardy (funkcje warunkowe)
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
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
Zagnieżdżone moduły
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!"
Atrybuty
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
Importowanie, używanie i aliasy modułów
Elixir oferuje kilka sposobów na zarządzanie modułami w kodzie:
- import: Importuje funkcje z innego modułu, dzięki czemu można je wywoływać bez podawania pełnej nazwy modułu.
- use: Umożliwia włączenie funkcjonalności z innego modułu.
- alias: Tworzy alias dla modułu, co skraca jego nazwę.
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
Struktury
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}}
Pozostałe elementy składni
Składnię jezyka Elixir w bardziej rozwiniętej formie, możecie znaleźć pod tym adresem.
Referencje
- Elixir v17.2
- From Ruby to Elixir
- Learn Functional Programming with Elixir
- Programming Elixir
- The Little Elixir & OTP Guidebook
- Designing Elixir Systems with OTP: Write Highly Scalable, Self-Healing Software with Layers
- No Fluff Jobs Log
- DevHints IO Cheat-sheet
- Learn Elixir in Y minutes
- WhatsApp, Discord, and the Secret to Handling Millions of Concurrent Users
- How Discord handles push request burst of over a million per minute with Elixirs GenStage