Типове и съпоставяне на образци
В предишната статия, “Защо Elixir?”, демонстрирахме някои основни синтактични структури в Elixir. Тази има за цел да е по-детайлна. Ще поговорим за определени, наистина базови свойства на езика. Разделяме статията на следните теми:
Интерпретатор на езика - 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
по-горе е един байт:
- Числото
5
е пакетирано в3
бита ->101
. - Числото
1
- един бит ->1
. - Числото
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
-а знаем:
- С него се дефинират променливи.
- С него могат да се правят проверки - дали дадена променлива има дадена стойност.
Променливите в 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
и въобще всички модули,
винаги, ВИНАГИ трансформират аргументите си като създават нови структури, никога не ги модифицират.
Това беше най-базовото което трябва да знаете преди да започнете да използвате езика по-сериозно. Не беше малко, но не беше и много сложно. Ако досега не сте се занимавали с функционален език, може да ви е доста странно. Не забравяйте, всяко непознато нещо винаги е леко страшно отначало. Това не означава че не е правилната стъпка напред във вашето развитие. Ще продължим с повече функции, модули от функции и рекурсия. Рекурсия се обяснява най-добре с рекурсия!