Spis treści
- Spis treści
- Małe przypomnienie
- Biblioteki
- Typy danych
- Pattern matching
- Sprawdzanie typów danych
- Listy
- Krotki
- Listy kluczy
- Mapy
- Funkcje anonimowe
- Arność funkcji
- Wyrażenia listowe
- Wyrażenia logiczne
- Moduły, funkcje imienne, warunkowe oraz rekurencja
- Przekazywanie (pipe)
- Enumeratory i strumienie
- Druga część artykułu
- Referencje
Małe przypomnienie
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.
Większy wstęp, znajdziecie pod tym adresem.
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 zmiennoprzecinkow
(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
etcCiągi znaków
(ang. string)Charlisty
(ang. charlist) - Tablica znaków np.~c'Foo'
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"}
Pattern matching
Pattern matching
w języku Elixir
to potężna funkcjonalność, która pozwala na przypisywanie wartości do zmiennych oraz na sprawdzanie struktury danych.
Można by pomyśleć, że poniższy zapis:
1 = 1 # Wynik => 1
To nic innego jak typowe przypisanie.. Jednakżę 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 (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.
Poniżej kilka dodatkowych przykładów:
Przypisywanie wartości
x = 1
# x teraz wynosi 1
Dopasowywanie list
[a, b, c] = [1, 2, 3]
# a = 1, b = 2, c = 3
Dopasowywanie krotek
{:ok, result} = {:ok, 42}
# result = 42
Dopasowywanie map
%{name: name, age: age} = %{name: "Alice", age: 30}
# name = "Alice", age = 30
Dopasowywanie z użyciem pin operatora (^)
x = 1
^x = 1
# Dopasowanie się powiedzie, ponieważ x wynosi 1
Dopasowywanie w funkcjach
defmodule Example do
def greet(%{name: name}) do
"Hello, " <> name
end
end
Example.greet(%{name: "Alice"})
# "Hello, Alice"
Pattern matching w Elixirze jest używany w wielu miejscach, takich jak przypisywanie zmiennych, funkcje, case expressions, i wiele innych. Pozwala to na bardziej deklaratywne i czytelne pisanie kodu.
Sprawdzanie typów danych
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.
Listy
Listy
(ang. lists
) definiujemy przy pomocy []
, a te mogą zawierać elementy różnych typów:
warehouse = ["XLM129", 56.123, 78.000, 2000, false, :open]
Enum.at(warehouse, 4)
Enum.at(warehouse, -1)
List.first(warehouse)
List.replace_at(warehouse, 0, "FLG23T")
[:Warsaw | warehouse]
warehouse ++ ["44-200", "own"]
List.delete(warehouse, "own")
hd(warehouse)
tl(warehouse)
[head | tail] = warehouse
head
tail
cameras = [:fujifilm, :sony, :hasselbad, :canon, :olympus]
[faz, daz, azz | rest] = cameras
faz
rest
Krotki
Krotki
(ang. tuples
) zapisywane są w pamięci w postaci ciągłej.
Sprawia to, że dostęp do elementów po indeksie lub sprawdzanie ich rozmiaru jest operacją szybką.
passion = {:ok, "photography", "devops", :coding}
tuple_size(passion)
elem(passion, 3)
Tuple.append(passion, "foo")
put_elem(passion, 1, :boo)
elem(passion, 1)
Listy kluczy
Listy kluczy
(ang. keywords lists
) w Elixirze to specjalny rodzaj listy, w której każdy element jest krotką (tuple) zawierającą dwa elementy: klucz i wartość. Klucze są zazwyczaj atomami.
Keyword lists są często używane do przekazywania opcji do funkcji.
params = [environment: :prod, users: [:mlog00, :wszc02]]
bad_params = [environment: :staging, users: [:mlog00, :wszc02]]
params[:users]
params[:environment]
defmodule FakeApp do
def init(args) do
cond do
args[:environment] == :prod -> {:ok, "Running in production environment."}
true -> {:err, "Not running in production environment"}
end
end
end
FakeApp.init params
FakeApp.init environment: :dev
list = [name: "Alice", age: 18]
name = Keyword.get(list, :name)
list = Keyword.put(list, :city, "New York")
list = Keyword.delete(list, :age)
has_name = Keyword.has_key?(list, :name)
Mapy
Mapy
(ang. maps
) w Elixirze to struktury danych, które przechowują pary klucz-wartość.
Są bardziej elastyczne niż keyword lists
, ponieważ klucze mogą być dowolnym typem danych, a nie tylko atomami.
Mapy tworzymy z pomocą keywordu: %{}
:
map = %{"name" => "Alice", :age => 18, 1 => "one"}
name = map["name"]
age = map[:age]
Map.has_key?(map, 1)
map = %{map | "name" => "Bob"}
Funkcje anonimowe
Funkcje anonimowe
w Elixirze, znane również jako lambdy
, są funkcjami, które nie mają przypisanej nazwy.
Są one często używane do krótkotrwałych operacji, które nie wymagają pełnej definicji funkcji nazwanej.
Funkcje anonimowe są definiowane za pomocą słowa kluczowego fn
i kończą się słowem end
.
Istnieje również możliwość używania syntaxu &1
, aczkolwiek nie jest on zbytnio lubiany i może znacząco utrudniać zrozumienie kodu w przypadku wielu argunemntów.
tax_value = fn (product_value) -> product_value * 0.23 end
console_tax_val = tax_value.(3999)
adder = fn (a, b) -> a + b end
res = adder.(500, 100)
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.
defmodule Math.Arrity.Example do
def add(a), do: a + a
def add(a, b) do
a + b
end
end
a = 2
# add/1
Math.Arrity.Example.add(a)
# add/2
Math.Arrity.Example.add(a, a)
Wyrażenia listowe
Język Elixir nie posiada klasycznych pętli for
, natomiast posiada możliwość iterowania po enumeratorach, często je filtrując i mapując w inną listę.
for n <- [3, 9, 40], do: n * (n-3)
Można także stosować zakresy (ang. range
):
for x <- 1..100, do: x * x
Lub filtrować dane:
envs = [prod: :storify, prod: :prefect, staging: :walld,
dev: :foox, uat: :d365, prod: :ally, staging: :pos]
for {:staging, val} <- envs, do: val
Wyrażenia logiczne
W języku Elixir mamy takie wyrażenia logiczne jak:
case
cond
if
unless
Case
Wyrażenie case
pozwala nam porównywać wartość z wieloma wzorcami, dopóki nie znajdziemy pasującego:
case {333, 666, 999} do
{1, 2, 3} -> "This will not work...."
{a, 666, 999} -> "Will match and bind 'a' to 333"
_ -> "Will match any value"
end
#case_ex_val = params[:environment]
case_ex_val = :dev
case case_ex_val do
case_ex_val when case_ex_val == :prod -> "Running on prod..."
_ -> "Undefined environment..."
end
Inny przykład z użyciem modułu oraz funkcji prywatnej oraz publicznej:
defmodule Example.Case.In.Func do
defp convert_binary_to_string(binary) do
binary
|> :binary.bin_to_list()
|> Enum.filter(&(&1 != 0))
|> List.to_string()
end
def binary_to_string(value) do
case value do
value when is_binary(value) == true -> convert_binary_to_string(value)
_ -> value
end
end
end
case_bin_example = <<83, 0, 48, 0, 48, 0, 49, 0>>
Example.Case.In.Func.binary_to_string(case_bin_example)
W tym przypadku funkcja konwertuje wszystko co ma typ binarny
na string
, resztę pozostawiając taką jaką jest.
Więcej o modułach i funkcjach, w następnym rozdziale.
If/unless
W przypadku Elixira, if/2
oraz unless/2
to makra (o których później) i raczej nie muszę ich tłumaczyć, po za unless
, które dla osób z poza świata Ruby
jest odwrotnością if
.
Przykłady:
envs = [environment: :staging]
if envs[:environment] == :staging do
"Running on staging"
end
if envs[:environment] == :staging do
"Running on staging as it should"
else
raise "ErlangError"
end
unless false do
"Error"
end
Jeśli chodzi o unless
to często jest nazywany jako: Evil twin brother (if). Jest spora grupa programistów z poza świata Ruby
skuszonych Elixirem, która niecierpi tego makra.
W przypadku gdybyście chceli użyć zagnieżdzonych if
czy unless
- lepszym wyborem zapewne będzie cond
:
temp = -21
cond do
temp < -60 -> "Vodka is starting to froze..."
temp == -33 -> "Could be better"
temp < -20 -> "Not great not terrible"
temp < -5 -> "It's warm..."
temp == 0 -> "It's okay."
temp in 5..20 -> "Global warming..."
temp >= 21 -> "We are going to die!"
true -> "Err"
end
Moduły, funkcje imienne, warunkowe oraz rekurencja
Rekurencja
Rekurencja
(ang. recursion
) to technika programowania, w której funkcja wywołuje samą siebie w celu rozwiązania problemu.
Jest to szczególnie przydatne w Elixirze, który nie posiada tradycyjnych pętli, takich jak for
czy while
.
Moduły
W tym przypadku przydatne stają się moduły
(ang. modules
) oraz funkcje nazwane
tudzież funkcje imienne
(ang. named functions
) oraz funkcje warunkowe
(ang. guard'y
).
W Elixirze moduły i funkcje nazwane są podstawowymi elementami strukturyzacji kodu.
Moduły pozwalają na grupowanie funkcji i definiowanie przestrzeni nazw, podczas gdy funkcje nazwane są funkcjami, które mają przypisane nazwy i mogą być wywoływane z różnych miejsc w kodzie.
Moduły denifiujemy z pomocą defmodule
, a funkcje nazwane z pomocą def
, natomiast guard'y
występują w funkcjach po nazwie funkcji oraz atrybutach np. when x = 0 do something
.
Funkcje imienne możemy definiować też one-linerami: def func, do: IO.puts("Something")
.
Obliczanie silni
defmodule Math do
def factorial(0), do: 1
def factorial(n) when n > 0 do
n * factorial(n-1)
end
end
Math.factorial(5)
Sumowanie elementów listy
defmodule Math.Calc do
def sum([]), do: 0
def sum([head | tail]) do
head + sum(tail)
end
end
Math.Calc.sum([2, 4, 6, 8])
Ciąg Fibonacciego
defmodule Math.Fib do
def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n) when n > 1 do
fibonacci(n - 1) + fibonacci(n - 2)
end
end
Math.Fib.fibonacci(10)
Odwracanie listy
defmodule BinaryTree do
defstruct value: nil, left: nil, right: nil
def search(nil, _value), do: false
def search(%BinaryTree{value: value}, value), do: true
def search(%BinaryTree{value: node_value, left: left, right: right}, value) do
if value < node_value do
search(left, value)
else
search(right, value)
end
end
end
tree = %BinaryTree{
value: 10,
left: %BinaryTree{value: 5},
right: %BinaryTree{value: 15}
}
BinaryTree.search(tree, 15)
Wartości domyślne w funkcjach
Wartości domyślne w funkcjach dodajemy w dosyć prosty sposób: x \\ "hello"
:
defmodule SomePlaneModule do
def do_something(arg \\ "hello") do
IO.inspect arg
end
end
Atrybuty modułów
Elixir posiada koncepcję atrybutów modułów (odziedziczone z Erlanga
).
Takie atrybuty, podobnie jak w Ruby
definiujemy z pomocą @
:
System.put_env("STORE_PWDX", "foo")
defmodule SomePlaneModule.Attributes.Example do
@password System.fetch_env!("STORE_PWDX")
def connect do
# :odbc.start()
# conn_str = "Driver={Oracle ODBC Driver};DBQ=srv:1521/xe;UID=usr;PWD=#{env};"
# conn = to_charlist(conn_str)
# {:ok, ref} = :odbc.connect(conn, [])
IO.puts "Boilerplate: Connecting to database with password: #{@password}"
end
end
SomePlaneModule.Attributes.Example.connect()
Uwagi odnośnie atrybutów
Generalnie atrybuty w Elixirze mają trzy zadania:
- Adnotacji dla modułów i funkcji.
- Jako tymczasowy storage na etapie kompilacji.
- Stałe typu
compite-time
.
Atrybuty zarezerwowane
Dodatkowo Elixir wspiera kilka zarezerwowanych atrybutów:
@moduledoc
— dokumentacja dla aktualnego modułu@doc
— dokumentacja dla funkcji w module (po atrybucie)@spec
— typ dla funkcji w module (po atrybucie)@behaviour
— używana przy OTP lub zdefiniowanym przez użytkownika ‘zachowaniu’ (ang.behaviour
)
Poniżej przykład bezpośrednio z dokumentacji Elixira:
defmodule SomePlaneModule.Attributes.Example.Math do
@moduledoc """
Provides math-related functions.
## Examples
iex> SomePlaneModule.Attributes.Example.Math.sum(1, 2)
3
"""
@doc """
Calculates the sum of two numbers.
"""
def sum(a, b), do: a + b
end
Przekazywanie (pipe)
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.
Enumeratory i strumienie
Większość operacji na danych, mimo, że język umożliwia Nam pisanie rekurencyjnego kodu - obsługujemy z pomocą modułów Enum
oraz Stream
.
Enumeratory
Enumenatory (ang. Enumerables
) udostępniają sporą ilość funkcji umożliwiających sortowanie, grupowanie, filtrowanie czy pobieranie elementów: list bądź map.
Cheatsheet
szeroko opisujący Enum’y można znaleźć pod tym adresem.
Poniżej kilka przykładów:
Enum.map([1, 2, 3], fn x -> x * 2 end)
5..50_000 |> Enum.map(fn x -> x * 2 end) |> Enum.sum()
Alternatywą dla Enum
‘ów są Stream
‘y, które są leniwe
(ang. lazy
).
Przydają się w pracy z z dużymi, możliwie nieskończonymi kolekcjami danych:
ex_stream = Stream.cycle([666..999])
Enum.take(ex_stream, 5)
Druga część artykułu
Dalsza część artykułu, znajduje się tutaj.