Структури
С помощта на речниците в Elixir можем да създаваме нещо като свои собствени типове. Това са Map-ове с име и точно определени ключове, които са обвързани с модул, чиито функции обикновено боравят с тях.
Почти всички функции за работа с речници могат да работят със структури, затова е добре да сте запознати с публикацията на тема речници, преди да прочетете тази.
Дефиниция
Структурите, както казахме, представляват ограничени речници. Те са ограничени по няколко признака:
- Ключовете им задължително са атоми.
- Ключовете им са предефинирани.
- Много свойства на речниците не са валидни за структури. Да речем, достъп от сорта на
some_struct[:some_atom]
е невъзможен.
Структурите се дефинират в модул. Името на модула е името на структурата:
defmodule Person do
defstruct [:name, :age, location: "Far away", children: []]
end
Създаваме структурата Person
с четири полета.
Полетата name
и age
нямат зададена определена default-на стойност, затова са nil
по подразбиране.
Създаваме инстанция на структурата по начин, подобен на този, с който създаваме речници:
pesho = %Person{name: "Пешо", age: 35, location: "Горен Чвор"}
#=> %Person{age: 35, children: [], location: "Горен Чвор", name: "Пешо"}
Лесно можем да видим какво представлява pesho
:
inspect(pesho, structs: false)
#=> "%{__struct__: Person, age: 35, children: [], location: \"Горен Чвор\", name: \"Пешо\"}"
Както виждаме, има скрит ключ __struct__
със стойност името на структурата.
Другата разлика с динамичните речници е, че структурите не имплементират някои протоколи, които ще видим в следващата статия.
Хубава новина е, че Map
модулът работи със структури:
Map.put(pesho, :name, "Стойчо")
#=> %Person{age: 35, children: [], location: "Горен Чвор", name: "Стойчо"}
Операторът за обновяване също работи:
%{pesho | name: "Стойчо"}
#=> %Person{age: 35, children: [], location: "Горен Чвор", name: "Стойчо"}
Също така:
is_map(pesho)
#=> true
is_map(%{"key" => "value"})
#=> true
map_size(pesho)
#=> 5
map_size(%{"ключ" => "стойност"})
#=> 1
Тези две Kernel
функции проверяват съответно дали стойност е речник и колко ключа има даден речник.
Функцията Kernel.is_map/1
връща true
и за структури.
А функцията Kernel.map_size/1
връща 5, макар pesho
да има само 4 дефинирани ключа.
Това е така, защото тя брои и скрития __struct__
ключ.
Можем да съпоставяме структури с речници и други структури от същия тип:
%{name: x} = pesho
#=> %Person{age: 35, children: [], location: "Горен Чвор", name: "Пешо"}
x
#=> "Пешо"
# Същото поведение:
%Person{name: x} = pesho
#=> %Person{age: 35, children: [], location: "Горен Чвор", name: "Пешо"}
x
#=> "Пешо"
#Но това не е валидно:
%Person{} = %{}
#=> ** (MatchError) no match of right hand side value: %{}
Последното поведение е нормално, защото Person
има __struct__
ключ, както и стойности по подразбиране.
Поради това никоя структура отляво не може да се съпостави с речник отдясно.
Обратното е възможно.
Понякога искаме при създаването на дадена структура задължително да се задават стойности за даден ключ, или с други думи, да имаме задължителни полета.
Това можем да постигнем чрез модулния атрибут @enforce_keys
:
defmodule Person do
@enforce_keys [:age, :name]
defstruct [:name, :age, location: "Far away", children: []]
end
%Person{name: "Гошо"}
#=> ** (ArgumentError) the following keys must also be given when building struct Person: [:age]
#=> expanding struct: Person.__struct__/1
%Person{name: "Гошо", age: 58, children: [%Person{name: "Пешо", age: 34}]}
#=> %Person{
#=> age: 58,
#=> children: [
#=> %Person{age: 34, children: [], location: "Far away", name: "Пешо"}
#=> ],
#=> location: "Far away",
#=> name: "Гошо"
#=> }
За какво и как да ползваме структури
Структурите се дефинират в модул – с идеята, че са нещо като тип, дефиниран от нас. В модула би трябвало да напишем специални функции, които да работят с този тип. Така имаме на едно място дефиницията на типа и функциите за работа с него.
Пример е MapSet
:
inspect(MapSet.new([2, 2, 3, 4]), structs: false)
#=> "%{__struct__: MapSet, map: %{2 => true, 3 => true, 4 => true}}"
Това е структура. Вътрешно е представена с речник под ключа map
.
За да има уникални стойности, тоест да е множество, използва ключовете на този речник като свои стойности.
При ключовете на речник, както знаем, няма повторения, затова той работи и стойностите са просто true
.
MapSet.union(MapSet.new([1, 2, 3]), MapSet.new([2, 3, 4]))
#=> #MapSet<[1, 2, 3, 4]>
Както споменахме, самият модул MapSet
дефинира набор от функции за работа със структурата MapSet
.
Това не означава, че можем да гледаме на структурите като на класове и на функциите в модулите им като на техни методи. Структурите не са класове. Elixir не е обектно-ориентиран език. Структурите са речници, които имат предварително дефинирана структура. Те са immutable и няма как тяхното състояние да се промени.
Ако създадем структура Post
, която представлява блог пост, ние просто създаваме речник с предварително дефинирани полета, представляващ публикация.
В модула му ще има функции за създаване на Post
от файл или от друг източник, като и функции за някаква валидация.
Няма нужда от сложни взаимодействия между структури, защото всичко e immutable.
Действията, с които разполагаме, са прости, малки функции, които композираме.
Структурите задават определена форма на данни, чиято конструкция познаваме. Ако ги комбинираме с тип-спецификациите, за които ще говорим в отделна публикация, получаваме добра документация на данните, с които работим. Именно поради това можем да приемаме структурите като дефинирани от нас типове.
Някои структури, идващи с Elixir
В предишната секция ви показахме MapSet
, която представлява множество.
Модулът на тази структура съдържа функции за работа с множества като сечения, разлики, сравнения.
Друга такава структура е поредицата (Range
), която вече сме показвали.
Всъщност, когато говорихме за енумерации, споменахме както Range
, така и MapSet
.
Как можем да направим наша структура енумерация, ще видим в следващата публикация.
Range
е поредица с начало и край:
range = 1..10
#=> 1..10
inspect(range, structs: false)
#=> "%{__struct__: Range, first: 1, last: 10}"
first..last = range
#=> 1..10
first
#=> 1
last
#=> 10
Поредицата е от цели числа, като ако я обходим, ще минем през всички цели числа от началото до края ѝ:
range = 1..100
#=> 1..100
# Сума от 1 до 100:
Enum.reduce(range, 0, fn n, acc -> n + acc end)
#=> 5050
Както знаем, unicode codepoint-ите са цели числа, така че:
?a..?z
#=> 97..122
?а..?я
#=> 1072..1103
Enum.map(?a..?z, &(&1 - 32)) |> Enum.map(&<< &1::utf8 >>)
#=> ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
#=> "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"]
Можем и да проверим дали дадено число се съдържа в дадена поредица:
5 in 1..10
#=> true
В Elixir регулярните изрази са имплементирани със структура и функциите за работа с тях са в модула Regex
.
Има съкратен синтаксис за създаване на регулярни изрази, но може да се използва и модулът:
phone_number = ~r/^\+?[\d\s]{3,}$/
#=> ~r/^\+?[\d\s]{3,}$/
{:ok, phone_number} = Regex.compile("^\\+?[\\d\\s]{3,}$")
#=> {:ok, ~r/^\+?[\d\s]{3,}$/}
inspect(phone_number, structs: false)
#=> %{
#=> __struct__: Regex,
#=> opts: "",
#=> re_pattern: {:re_pattern, 0, 0, 0,
#=> <<69, 82, 67, 80, 113, 0, 0, 0, 16, 0, 0, 0, 1, 0, 0, 0, 255, 255, 255, 255,
#=> 255, 255, 255, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 0, 0, 0, 0, 0, 0, 0,
#=> ...>>},
#=> re_version: "8.41 2017-07-05",
#=> source: "^\\+?[\\d\\s]{3,}$"
#=> }
Има и специален оператор за match-ване на регулярни изрази, =~
:
"+359 887 172 213" =~phone_number
#=> true
"0887172213" =~ phone_number
#=> true
"31" =~ phone_number
#=> false
Няма да навлизаме в подробности за регулярните изрази. За повече информация погледнете документацията на модула Regex
.
Регулярните изрази на Elixir ползват модула на Erlang :re
и са perl compatible.
Други структури, които ще ползвате често, са Date
, Time
и DateTime
.
Ето пример за Time
:
time = ~T[23:00:07.001]
#=> ~T[23:00:07.001]
inspect(time, structs: false)
#=> %{
#=> __struct__: Time,
#=> calendar: Calendar.ISO,
#=> hour: 23,
#=> microsecond: {1000, 3},
#=> minute: 0,
#=> second: 7
#=> }
Има и много други структури, идващи с езика, както и такива, които ще срещате и използвате, когато използвате различни библиотеки. В следващата статия ще разберем как да направим така, че различни структури да бъдат подавани на една и съща функция, а също и – спрямо това коя е структурата – да се изпълнява различна логика – по начин, който е разширим за нови структури.