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


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

Числа

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

1    # В десетична бройна система
#=> 1
10_000
#=> 10000

0x53 # В шестнадесетична
#=> 83
0o53  # В осмична
#=> 43
0b11 # В двоична
#=> 3

3.14 # С плаваща запетая
#=> 3.14
1.0e-10 # С плаваща запетая
#=> 1.0e-10

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

1 + 41
#=> 42
21 * 2
#=> 42

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

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

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

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

is_integer(3)  # За проверка на типа - дали е цяло число
#=> true
is_integer(3.0)
#=> false
is_float(3.0) # Дали е с плаваща запетая
#=> false
is_number(3.0) # Има и проверка дали е число изобщо
#=> true
is_number(3)
#=> true

round(3.2) # За закръгляне
#=> 3
round(3.8) # За закръгляне
#=> 4
trunc(3.8) # За отрязване
#=> 3

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

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

true
#=> true
false
#=> false

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

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(@).
  • Атомите могат да завършват на ! или на ?.
  • Идеални за pattern matching (съпоставянето им е еквивалентно на съпоставяне на числа)

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

:atom
#=> :atom

:true
#=> true

:anoter_atom
#=> :anoter_atom

SomeModule # Може и да не е дефиниран
#=> SomeModule
SomeModule == :"Elixir.SomeModule" # Това е истинското име на модула
#=> true

is_atom(:atom)
#=> true
is_atom(true)
#=> true

true == :true
#=> true

:"atom with a space" # Могат да се дефинират и така
#=> :"atom with a space"

Низове

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

"Здрасти"
#=> "Здрасти"
"Здрасти #{:Pesho}" # Интерполация
#=> "Здрасти Pesho"

"""
Един
стринг
на
повече
от един ред
"""
#=> "Един\nстринг\nна\nповече\nот един ред" # Поддръжа на множество редове

is_binary("Здрасти") # Низовете представляват поредица от байтове
#=> true
String.length("Здрасти") # Брой на символи
#=> 7
byte_size("Здрасти") # Брой на байтове
#=> 14

"Бял" <> " мерцедес!" # Конкатенация
#=> "Бял мерцедес!"

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

Списъци

Дефинират се така:

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

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

Кортежи

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

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

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

Keyword lists

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

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

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

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

Ключовете им могат да се повтарят.

Пример String.split/3:

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

Речници

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

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

Двоичен тип (Binaries)

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

<< 2 >> # Цялото число 2 в 1 байт
#=> <<2>>
byte_size << 2 >>
#=> 1

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

byte_size << 1, 2 >>
#=> 2

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

<< 5::size(3), 1::size(1), 5::size(4) >>
#=> <<181>>
0b10110101
#=> 181

byte_size << 5::size(3), 1::size(1), 5::size(4) >>
#=> 1

is_bitstring << 5::size(3), 1::size(1) >>
#=> true
is_binary << 5::size(3), 1::size(1) >> # Не е цял байт. Трябва броят на битовете да е кратен на 8.
#=> false

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

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

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

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

fn (x) -> x + 1 end
#=> #Function<6.52032458/1 in :erl_eval.expr/5>

(fn (x) -> x + 1 end).(4) # Извикване
#=> 5

is_function((fn (x) -> x + 1 end))
#=> true

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

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

Други типове

Други типове са Port, който се използва за комуникация с OS-level процеси и IO (като цяло с външния свят), Reference, който се използва за уникални стойности в рамките на виртуалната машина и PID, който се използва с Erlang/Elixir процеси.

Езикът съдържа синтаксис за регулярни изрази и работа с тях (~r/\w+/im), както и ranges (1..1000), но това не са отделни типове, а структури, които пък са речници. Ще ги разгледаме в следваща публикация

Тази статия има за цел да представи различните типове на едно базово ниво. Когато навлезем в езика ще се запознаем по-подробно с тях. Ще има публикации (както споменахме) за определени типове, а като цяло ще ги ползваме в различни упражнения.

Добро начало за опознаване на възможностите на езика е докментацията на Kernel модула, която можете да намерите тук.

Сравняване и типове

Ето и пълен списък с операторите за сравнение, както е дефиниран в документацията на Erlang:

ОперацияКакво прави
==Сравнява аргументите си дали са равни
!=Сравнява аргументите си дали са различни
=<Връща true ако първият ѝ аргумент е по-малък или равен на втория
< Връща true ако първият ѝ аргумент е по-малък от втория
>=Връща true ако първият ѝ аргумент е по-голям или равен на втория
> Връща true ако първият ѝ аргумент е по-голям от втория
===Сравнява аргументите си дали са равни и от един и същ тип
!==Сравнява аргументите си дали са различни и от различни типове

Има подредба между типовете:

ТипПодредба спрямо долния ред
число <
атом <
reference стойност <
функция <
порт <
pid <
кортеж <
речник <
nil <
списък <
двоична стойност

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

Примери с различни типове:

1 > :c
#=> false

%{1 => 3} > %{1.0 => 3}
#=> false

%{one: 1, four: 4} == %{one: 1.0, four: 2.0}
#=> false

%{one: 1, four: 4} == %{one: 1.0, four: 4}
#=> true

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

В идеалния свят, когато извикаме функция с една и съща стойност хиляда пъти, трябва да получим един и същи резултат хиляда пъти. И светът не трябва да се променя тайно от нас. Трябва да остане същият - познат и предвидим. На практика, обаче, не всичко е идеално. Това не ни спира да се стремим към нашите идеали.

Обикновено искаме да направим колкото се може по-просто и бързо нещата и те да работят. Колкото повече неща се променят в нашата програма, докато тя върви към целта си, за толкова повече неща трябва да мислим, да дебъгваме, да ги търсим из кода и паметта.

Защо да си го причиняваме, когато нашата програма може да е просто множество композирани функции, които не изменят състояния? Тази идея е залегнала във функционалното програмиране. Трудно е да не променяме целия свят (макар и не невъзможно, може да имаме поредица от познати светове), но винаги е по-лесно да не променяме това, което знаем как да променим или да не променим.

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

Сега въпросът е - това не е ли неефективно? Да речем имаме си един списък от хиляда елемента, искаме да вдигнем всеки от тях на квадрат. Как става това? Ами строим нов списък с квадратите, а старият си остава непроменен.

Но не всеки път нещата са такива, Elixir прeизползва каквото може от базовата структура, когато прави нова. Все пак базовата структура също е immutable, няма да се промени с времето.

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

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

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