Типове и съпоставяне на образци


В предишната статия, “Защо Elixir?”, демонстрирахме някои основни синтактични структури в Elixir. Тази има за цел да е по-детайлна. Ще поговорим за определени, наистина базови свойства на езика. Разделяме статията на следните теми:

  1. Интерпретатор на езика - IEx
  2. Основни типове
  3. Съпоставяне на образци
  4. Неизменимост

Интерпретатор на езика - IEx

Първото нещо което трябва да направим, когато започнем да изучаваме нов език за програмиране е да имаме средствата, нужни да изпълняваме програмите си. Това значи че трябва да инсталираме компилатор/интерпретатор и инструменти за работа с езика. В зависимост от операционната ви система и средата, която ползвате, инсталацията ще протече по различен начин.

Тази статия няма за цел да ви научи как да инсталирате езика и инструментите за работа с него - това е строго индивидуално. Поради това е най-добре да ползвате официалналната страница за инсталация.

Изискванията за тази и следващите статии са:

  • Erlang/OTP >= 19
  • Elixir >= 1.4
  • Удобен за вас редактор или IDE
  • Основни познания по GIT и инсталиран GIT

Нека приемем че отговаряте на изискванията и имате инсталиран Elixir. От това следва, че имате командата iex, която можете да изпълните в терминала на компютъра си. Ако всичко е наред, ще видите нещо подобно:

❯ iex
Erlang/OTP 19 [erts-8.2] [source] [64-bit] [smp:4:4] [async-threads:10] [kernel-poll:false]

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

IEx е съкращение от ‘Interactive Elixir’ и е интерпретатор на езика. Всяка команда която изпълните в iex се интерпретира и стойността ѝ се отпечатва. Пример:

iex(1)> 1 + 1
2

Няколко удобни възможности на iex:

BREAK меню

Как да излезем от iex? Да, важно е да знаем как да излезем от интерпретатора. При натискане на CTRL-c няма да излезете от iex. Ще ви излезе меню. Това меню ви дава няколко опции. Ако тук за втори път натиснете CTRL-c или просто a, вече ще излезете от програмата. Между другото, това меню (наречено BREAK меню) предлага интересни опции - информация за iex средата, за процесите в нея, и други.

User Switch меню

Друго интересно меню е User Switch менюто. В него можем да влезем с CTRL-g. Това меню може да бъде използвано за излизане от iex (да, още един начин). CTRL-g и q излиза от iex.

Интересна възможност е стартирането на друга iex сесия:

User switch command
--> s 'Elixir.IEx'
--> c

Това ще инициализира нова iex сесия. Тя ще е напълно изолирана от предишната. Каквото и да се дефинира в едната, няма да е видимо в другата. Можем да се върнем към първоначалната сесия с CTRL-g и c 1. С j в User Switch менюто виждаме списък от сесиите, както и коя е активна в момента. Пример:

 --> j
1  {erlang,apply,[#Fun<Elixir.IEx.CLI.1.112225073>,[]]}
2* {'Elixir.IEx',start,[]}

С CTRL-g и h ще видим възможните команди.

Отдалечени сесии

IEx може да се свърже с вече съществуваща iex сесия. Това е възможно, даже ако сесията е на друга машина. Има едно условие. Сесиите към които се свързваме трябва да са именовани. Можем да пуснем именована iex сесия с:

iex --sname one
iex(one@meddland)1> node() # Връща име@хост на сесията
:one@meddland

Нека в друг терминал да стартираме друга сесия.

iex --sname two
iex(two@meddland)1> node() # Връща име@хост на сесията
:two@meddland

И сега нека от сесия two да се свържем към сесия one с CTRL-g:

User switch command
--> r 'one@meddland' 'Elixir.IEx'
--> c

Interactive Elixir (1.4.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(one@meddland)1>

Ако в сесия one бяхме дефинирали модули и функции, тук те биха били достъпни. Има и съкратен вариант на стартиране на сесия и свързване с нея:

iex --sname two --remsh one@meddland

Важни функции за ползване в iex

Ако в iex напишем просто h и го изпълним ще видим списък с функции, които можем да ползваме. Ето някои от тях:

За компилиране - c.

iex(1)> c "path/to/file.ex"  # Компилира файла намиращ се на зададеното място.

По този начим можем да тестваме през iex функционалност, която сме записали във файл.

Подобна е и r, но тя re-компилира кода на даден модул.

Друга интересна функция е i.

iex(1)> i 1
Term
  1
Data type
  Integer
Reference modules
  Integer

Отпечатва информация за типа на аргумента си.

Функцията h може да приема функция. Това ще отпечата документацията на функцията.

iex(1)> h is_integer

def is_integer(term)

Returns true if term is an integer; otherwise returns false.

Allowed in guard tests. Inlined by the compiler.

На кратко това е за iex засега. Ще го използваме за демонстрация на примери, a с времето ще ви запознаем и с други негови свойства и функции. Сега, като имаме представа как да изпълняваме Elixir код и да виждаме резултата от това, можем да започнем с навлизането в синтаксиса му. В примерите които следват ще заменим iex(n)> с просто iex> за по лесно четене.

Основни типове

Да си говорим какви типове предлага даден език не е много интересно, но пък е нужно да знаем с какво разполагаме. Ще се опитаме да ви представим някои от типовете набързо, за да можем да продължим с по-интересни неща.

Числа

Elixir предлага цели числа и числа с плаваща запетая:

iex> 1    # В десетична бройна система
1
iex>10_000
10000
iex> 0x53 # В шестнадесетична
83
iex> 0o53  # В осмична
43
iex> 0b11 # В двоична
3
iex> 3.14 # С плаваща запетая
3.14
iex> 1.0e-10 # С плаваща запетая
1.0e-10

При аритметичните операции с тях, може да ви изненада поведението на оператора /.

iex> 1 + 41
42
iex> 21 * 2
42
iex> 54 / 6 # Връща резултат с плаваща запетая
9.0
iex> div(54, 6) # В повечето езици `/` прави това
9
iex> rem 11, 3 # А ето как получаваме остатъка.
2

Езикът съдържа и операции за сравнение:

iex> 1 < 2
true
iex> 1 <= 2
true
iex> 1 >= 1
true
iex> 1 > 1
false
iex> 1 != 2
true
iex> 1 == 2
false
iex> 1 == 1.0 # Операторът == сравнява по стойност
true
iex> 1 === 1.0 # Операторът === сравнява по стойност И тип
false
iex> 1 !== 1.0 # Операторът !== сравнява по стойност И тип
true

Няколко полезни функции свързани с числа:

iex> is_integer(3)  # За проверка на типа - дали е цяло число
true
iex> is_integer(3.0)
false
iex> is_float(3.0) # Дали е с плаваща запетая
false
iex> is_number(3.0) # Има и проверка дали е число изобщо
true
iex> is_number(3)
true
iex> round(3.2) # За закръгляне
3
iex> round(3.8) # За закръгляне
4
iex> trunc(3.8) # За отрязване
3

Съществуват специални модули - Integer и Float предлагащи функции за работа с числа. Няма ограничение за големината на целите числа.

Булеви стойности: true/false

Няма какво толкова да се покаже тук.

iex> true
true
iex> false
false
iex> is_boolean(true)
true
iex> is_boolean(0)
false
iex> true == false
false

Операторите за работа с булеви стойности са and, or, &&, ||, not и !. Разликата между and и &&, както между or и ||, a и между not и ! e, че and, or и not приемат само булеви стойности. Те са стриктни и ако им се подаде нещо различно от true и false (:true и :false, както ще видим след малко), ще се хвърли ArgumentError. В Elixir само false и nil се приемат за falsey стойности.

Атоми

Атомите са константи, чието име е стойността им.

  • Булевите стойности true и false всъщност са атомите :true и :false
  • Имената на модули (колекции от функции и нещо повече) в Elixir също са атоми.
  • Модули идващи от Erlang са реферирани от атоми.
  • Удобни са за ползване като ключове в map-ове.
  • Задължителна част от keyword lists.
  • Често се използват в кортежи за означаване на резултат от функция. Пример - {:ok, 2}
  • Освен ако не са в двойни кавички, атомите могат да съдържат подчертавки, цифри и латински букви, както и at(@).
  • Атомите могат да завършват на ! или на ?.

Атомите са важна част от Elixir. За запознатите с Ruby - еквивалентни са на символите.

iex> :atom
:atom
iex> :true
true
iex> :anoter_atom
:anoter_atom
iex> SomeModule # Може и да не е дефиниран
SomeModule
iex> is_atom(:atom)
true
iex> is_atom(true)
true
iex> true == :true
true
iex> :"atom with a space" # Могат да се дефинират и така
:"atom with a space"

Низове

Низовете в Elixir се дефинират с двойни кавички и са с UTF-8 encoding:

iex> "Здрасти"
"Здрасти"
iex> "Здрасти #{:Pesho}" # Интерполация
"Здрасти Pesho"
iex> "Един
...> стринг
...> на
...> повече
...> от един ред"
"Един\nстринг\nна\nповече\nот един ред" # Поддръжа на множество редове
iex> is_binary("Здрасти") # Низовете представляват поредица от байтове
true
iex> String.length("Здрасти") # Брой на символи
7
iex> byte_size("Здрасти") # Брой на байтове
14
iex> "Бял" <> " мерцедес!" # Конкатенация
"Бял мерцедес!"

За удобна работа с UTF-8 низове, съществува модул String. В една от следващите статии ще се запознаем с тях по-подробно.

Списъци

Списъците представляват свързани списъци. Дефинират се така:

iex> [1, 2, "три", 4.0] # Не са хомогенни
[1, 2, "три", 4.0]
iex> length [1, 2, 3, 5, 8] # Дължината
5
iex> hd [1, 2, 3, 5, 8] # Връща първия елемент (head)
1
iex> tl [1, 2, 3, 5, 8] # Връща списък с елементите без първия (tail)
[2, 3, 5, 8]
iex> is_list([1, 2])
true
  • Списъците са важна структура, има специален модул, List, за работа с тях.
  • Не държат стойностите си подредени в паметта.
  • Намирането на дължината им, четене на стойност по index, добавяне на стойност на index и триене на стойност на index са все линейни операции.

В една от следващите статии ще говорим по-подробно за тях.

Кортежи

Кортежите подобно на списъците могат да съдържат стойности от всякакъв тип.

iex> {:ok, 7}
{:ok, 7}
iex> tuple_size({:ok, 7, 5})
3
iex> is_tuple({:ok, 7, 5})
true
  • Кортежите съхраняват елементите си подредени един след друг в паметта.
  • Достъпът до елемент по индекс и взимането на дължината им са константни операции.
  • Ползват се за много неща:

    • Заедно с атомите за връщане на множество стойности от функция.
    • За pattern matching - ще видим малко по-долу.
    • Read-only колекция, защото писането в тях е скъпа операция.

Keyword lists

За тези списъци е по-добре да ползваме английското понятие (иначе са асоциативни списъци). Като цяло това са списъци, които съдържат tuple-и от по два елемента. Всеки кортеж има за първи елемент атом - ключ.

iex>[{:one, 1}, {:two, 2}]
[one: 1, two: 2]  # Както виждате има специален синтаксис за тях. Това е същото:
iex> [one: 1, two: 2]
[one: 1, two: 2]

Главно се използват за keyword аргументи на функции. Ако keyword list е последен аргумент на функция, можем да пропуснем квадратните скоби при извикване:

iex> f(1, 2, three: 3, four: 4)

Ключовете им могат да се повтарят. Използват се и за предаване на command line параметри или опции на функции. Пример е String.split/3.

iex> String.split("one,two,,,three,,,four", ",", trim: true)
["one", "two", "three", "four"] # Няма празни низове заради опцията trim: true.

Maps

Колекции от ключове и стойности.

  • Map-овете в Elixir не позволяват еднакви ключове.
  • За ключове може да се използва всичко и дори няма нужда да бъдат един и същи тип, но обикновено се използват низове или атоми.
  • Може би на Български език бихме ги нарекли речници.
iex(49)> %{"one" => 1, "two" => 2}
%{"one" => 1, "two" => 2}
iex(50)> %{one: 1, two: 2} # Ако ключовете са атоми - има опростен начин за създаване.
%{one: 1, two: 2}

Бинарен тип (Binaries)

Прдставляват поредици от битове и байтове.

iex> << 2 >> # Цялото число 2 в 1 байт
<<2>>
iex> byte_size << 2 >>
1
iex> << 255 >> # Цялото число 255 в 1 байт
<<255>>
iex> << 256 >> # Превърта и става 0
<<0>>
iex> <<1, 2>> # Две цели числа в два байта.
<<1, 2>>
iex> byte_size << 1, 2 >>
2

Не е задължително едно поле да е един байт, това може да се управлява:

iex> << 5::size(3), 1::size(1), 5::size(4) >>
<<181>>
iex> 0b10110101
181
iex> byte_size << 5::size(3), 1::size(1), 5::size(4) >>
1

Цялото binary по-горе е един байт:

  1. Числото 5 е пакетирано в 3 бита -> 101.
  2. Числото 1 - един бит -> 1.
  3. Числото 5 сега е пакетирано в 4 бита -> 0101 Общо 8 бита - 1 байт, и същото като 0b10110101 -> 181.

Интересн факт - низовете в Elixir са имплементирани като binary тип. Спомняте си че is_binary("Стринг") връщаше true.

Анонимни функции

В предната статия ги представихме, нека си припомним:

iex> fn (x) -> x + 1 end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> (fn (x) -> x + 1 end).(4) # Извикване
5
iex> is_function((fn (x) -> x + 1 end))
true

Има и друг начин за дефиниране на анонимни функции:

iex> &(&1 + 1) # Тук &(тяло) е дефиницията на функцията. &1 в тялото значи 'първи параметър'
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> (&(&1 + 1)).(4)
5

Други типове

Други типове са Port, Reference и PID, които се използват с процеси.

Тази статия има за цел да представи различните типове на едно базово ниво. Когато навлезем в езика ще се запознаем по подробно с тях. Ще има лекции/статии (както споменахме) за определени типове, а като цяло ще ги ползваме в различни упражнения. Добро начало за опознаване на възможностите на езика е докментацията на Kernel модула - https://hexdocs.pm/elixir/1.4.2/Kernel.html.

Съпоставяне на образци

Преводът на pattern matching от Английски на Български език е Съпоставяне на образци. Както сигурно си мислите - звучи странно и някак далечно. За конспекта на курса по Elixir беше нужно различните термини да са на Български, затова от време на време ще го ползваме в заглавия и под-заглавия, но когато говорим за него ще ползваме просто съпоставяне, matching или pattern matching.

В Elixir pattern matching-a е еднa от най-важните и основни особености. Операторът = се нарича match operator. Можем да го сравним с знака = в математиката. Използвайки го, превръщаме целия израз в уравнение, в което сравняваме лявата с дясната страна. Ако сравнението е успешно се връща стойността на това уравнение, ако не - има грешка.

Нека стартираме нова iex сесия и да упражним оператора =:

iex> x = 5 # x приема стойност 5 и сравнението е успешно.
5
iex> 5 = x # Тъй като x e 5, 5 e равно на 5 и сравнението е успешно.
5
iex> 4 = x # Тъй като x e 5, 4 e различно от 5 и сравнението не е успешно. Хвърля се MatchError.
** (MatchError) no match of right hand side value: 1

Съпоставянето може да се използва за проверка на очаквани параметри. То е начин за разклоняване на логиката в Elixir.

Тази статия има за цел да ви запознае с основни инструменти и похвати при писането на Elixir код. По нататък ще разширим познанията си за тях, докато научаваме нови неща. Засега за match operator-а знаем:

  1. С него се дефинират променливи.
  2. С него могат да се правят проверки - дали дадена променлива има дадена стойност.

Променливите в ELixir са от типа на стойността си. Те не могат просто да се декларират без да им се зададе стойност.

Имената на променливи задължително започват с малка латинска буква или подчертавка (_), следвана от букви, цифри или подчертавки. Могат да завършват на ? или !. Операторът = ще опита да присвои на всички възможни променливи от ляво стойности от дясно.

iex> {one, tWo, t3, f_our, five!} = {1, 2, 3, 4, 5}
{1, 2, 3, 4, 5}
iex> one
1
iex> tWo
2
iex> t3
3
iex> f_our
4
iex> five!
5

Това е абсолютно валидно съпоставяне и присвояване на стойностти от дясно към променливите от ляво. Забелязвате как използваме tuple за да свържем няколко променливи със стойности. Можем да го направим и със списък да речем:

iex(107)> [head|tail] = [1, 2, 4, 5]
[1, 2, 4, 5]
iex> head
1
iex> tail
[2, 4, 5]
iex> [a, b|tail] = [1, 2, 4, 5]
[1, 2, 4, 5]
iex> a
1
iex> b
2
iex> tail
[4, 5]

Друго интересно приложение на pattern matching-a - анонимна функция която се държи различно с различни параметри:

iex> g = fn
...>   0 -> 0
...>   x -> x - 1
...> end
#Function<6.52032458/1 in :erl_eval.expr/5>
iex> g.(0)
0
iex> g.(3)
2

Функцията съпоставя стойността с която е извикана с условията си от горе на долу. Ако я извикаме с 0, първото условие е успех и функцията връща 0. Във всички други случаи връща x - 1.

В Elixir e възможно да променим стойността на променлива. В Erlang това не е възможно. Ако искаме една променлива, която вече съществува да не промени стойността си при съпоставяне, а да се направи съпоставка с текущата и стойност и да се хвърли MatchError ако не е успешна, можем да използваме pin оператора - ^.

iex> x = 5
5
iex> ^x = 6 # Променливата се съпоставя с текущата си стойност и не приема новата преди съпоставката => грешка
** (MatchError) no match of right hand side value: 6

Интересно свойство е следното: Ако искаме да променим y само ако x е точно определена стойност, можем да го направим така:

iex> {y, ^x} = {5, 4} # Тук y ще стане 5 само и единствено ако x е 4, иначе ще имаме MatchError

Ако се опитаме да присвоим стойност на unbound променлива (досега не е съществувала), използвайки pin оператора, ще получим грешка.

iex> ^z = 4
** (CompileError) iex:56: unbound variable ^z

Както знаете се опитваме да превеждаме различни термини на Български език, но оператор-карфица или още по лошо - оператор-габърче, звучи нелепо. Доста от типовете, операторите и термините ще ползваме на Английски езикв статиите занапред.

Неизменимост (Immutablility)

Както вече няколко пъти казахме (а и демонстрирахме с код), Elixir e функционален език. Така че ако се чудите къде са ви класовете, интерфейсите, йерархиите и мутаторите, спрете да се чудите! Няма ги. Това което ще ви покажем в следващата статия са модули - колекции от функции. В идеалния свят когато извикаме функция с една и съща стойност, да речем, хиляда пъти, трябва да получим един и същи резултат - хиляда пъти. И светът не трябва да се променя тайно от нас. Трябва да остане същият - познат. Не всичко е идеално на практика, обаче. Това не ни спира да се стремим към нашите идеали. Ние програмистите сме мързеливи хора като цяло. Искаме да направим колкото се може по-просто и бързо нещата и те да работят. Колкото повече неща се променят в нашата програма, докато тя изпълнява целта си, за толкова повече неща трябва да мислим, да дебъгваме, да ги търси из кода и паметта. Защо да си го причиняваме, когато нашата програма може да е просто множество композирани функции, които не изменят състояния? Тази идея е залегнала във функционалното програмиране, трудно е да не променяме целият свят (макар и не невъзможно, може да имаме поредица от познати светове), но винаги е по лесно да не променяме това което знаем как да променим или да не променим.

Веднъж създадена една структура от данни не трябва да може да бъде променяна. Хубаво е да имаме функции, които създават една структура с база друга, но това е всичко от което се нуждаем. След всичко казано по-горе е време, да ви споделим един факт. Всикчи типове които видяхте до тук, всички структури и колекции от данни са точно такива - непроменими (immutable).

Сега въпросът е - това не е ли неефективно? Да речем имаме си един списък от хиляда елемента, искаме да вдигнем всеки от тях на квадрат. Как става това? Ами строиме нов списък с квадратите, а старият си остава непроменен. Но не всеки път нещата са такива, Elixir призползва каквото може от базовата структура, когато прави нова. Все пак базовата структура също е immutable, няма да се промени с времето.

iex> base_list = [1, 2, 3]
[1, 2, 3]
iex> new_list = [0 | base_list]
[0, 1, 2, 3]

В примера новият списък преизползва за всичките си елементи освен първия базовия списък. Важното е да запомните че функциите идващи от модули като List, Enum, String и въобще всички модули, винаги, ВИНАГИ трансформират аргументите си като създават нови структури, никога не ги модифицират.

Това беше най-базовото което трябва да знаете преди да започнете да използвате езика по-сериозно. Не беше малко, но не беше и много сложно. Ако досега не сте се занимавали с функционален език, може да ви е доста странно. Не забравяйте, всяко непознато нещо винаги е леко страшно отначало. Това не означава че не е правилната стъпка напред във вашето развитие. Ще продължим с повече функции, модули от функции и рекурсия. Рекурсия се обяснява най-добре с рекурсия!