Elixir: Введение в структуры
Для моделирования данных, которые лучше отражает предметную область, используют структуры.
Структуры по сути словари, но с некоторыми особенностями. Структуры проверяются на уровне компиляции, в них можно задать значения по умолчанию и структурам недоступны протоколы для словарей, например протоколы Enum
. Протоколы рассмотрим чуть дальше, а сейчас изучим структуры:
defmodule Counter do
defstruct state: 0
end
my_counter = %Counter{}
%Counter{state: 0}
В примере мы объявили модуль Counter
и в нем объявили, что этот модуль является структурой с полем state
, которое по умолчанию равно 0
. Затем мы связали my_counter
с ранее объявленной структурой. Еще мы можем объявить поля без значения по умолчанию:
defmodule Counter do
defstruct [:current, initial: 0, hello: "world"]
end
defmodule Counter2 do
defstruct current: nil, initial: 0, hello: "world"
end
my_counter = %Counter{}
# => %Counter{current: nil, initial: 0, hello: "world"}
my_second_counter = %Counter2{}
# => %Counter2{current: nil, initial: 0, hello: "world"}}
Теперь рассмотрим, как работать с полями структур:
defmodule User do
defstruct age: 0, name: "John"
end
john = %User{age: 20}
john.age
# => 20
john.name
# => "John"
alice = %{john | name: "Alice"}
alice.name
# => "Alice"
jane = %{alice | other_field: "some"}
# => ** (KeyError) key :other_field not found in: %User{age: 20, name: "Alice"}
# => (stdlib 4.3.1.1) :maps.update(:other_field, "some", %User{age: 20, name: "Alice"})
Используя синтаксис модификации |
, виртуальная машина заранее знает, что структура имеет фиксированное количество полей и не дает добавлять новые, только менять заранее объявленные. Интересный момент, что структуры можно использовать в паттерн-матчинге:
defmodule Pet do
defstruct name: "Barkley"
end
defmodule User do
defstruct name: "John"
end
defmodule Example do
def print_name(%User{name: name} = user) do
"Hello, human #{name}"
end
def print_name(%Pet{name: name} = pet) do
"Hello, pet #{name}"
end
end
pet = %Pet{}
user = %User{}
Example.print_name(pet)
# => "Hello, pet Barkley"
Example.print_name(user)
# => "Hello, human John"
Благодаря паттерн-матчингу мы добились полиморфного поведения функции.
Так как структуры не используют протоколы словарей, то и доступ к полю через []
становится недоступным:
defmodule User do
defstruct age: 0, name: "John"
end
user = %User{}
# => %User{age: 0, name: "John"}
user.name
# => "John"
user[:name]
# => ** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour. If you are using get_in/put_in/update_in, you can specify the field to be accessed using Access.key!/1)
# => User.fetch(%User{age: 0, name: "John"}, :name)
# => (elixir 1.15.0) lib/access.ex:305: Access.get/3
# => iex:201: (file)
Map.get(user, :name)
# => "John"
Еще с помощью атрибута enforce_keys
можно добавить проверку на инициализацию данных по ключу при создании структуры:
defmodule User do
@enforce_keys [:name, :age]
defstruct [:name, :age]
end
%User{}
# => ** (ArgumentError) the following keys must also be given when building struct User: [:name, :age]
# => expanding struct: User.__struct__/1
# => iex:204: (file)
%User{name: "John"}
# => ** (ArgumentError) the following keys must also be given when building struct User: [:age]
# => expanding struct: User.__struct__/1
# => iex:205: (file)
%User{name: "John", age: 20}
# => %User{name: "John", age: 20}
Такая проверка работает только во время компиляции и только при создании структуры. Проверка не будет запускаться при изменении данных по ключу.
Задание
Создайте функцию calculate_stats
, которая подсчитывает, сколько в списке людей и питомцев:
users_and_pets = [%User{}, %User{}, %Pet{}]
Solution.calculate_stats(users_and_pets)
# => %{humans: 2, pets: 1}
Solution.calculate_stats([])
# => %{humans: 0, pets: 0}
only_pets = [%Pet{}, %Pet{}, %Pet{}]
# => %{humans: 0, pets: 3}
Обратите внимание, что структуры в модуле заранее определены.
Упражнение не проходит проверку — что делать? 😶
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
- Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Мой код отличается от решения учителя 🤔
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Прочитал урок — ничего не понятно 🙄
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.