Бесплатный курс по Elixir. Зарегистрируйтесь для отслеживания прогресса →

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}

Обратите внимание, что структуры в модуле заранее определены.

Упражнение не проходит проверку — что делать? 😶

Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:

  • Обязательно приложите вывод тестов, без него практически невозможно понять что не так, даже если вы покажете свой код. Программисты плохо исполняют код в голове, но по полученной ошибке почти всегда понятно, куда смотреть.
В моей среде код работает, а здесь нет 🤨

Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.

Мой код отличается от решения учителя 🤔

Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.

В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.

Прочитал урок — ничего не понятно 🙄

Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.

Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.


Нашли ошибку? Есть что добавить? Пулреквесты приветствуются https://github.com/hexlet-basics
Если вы столкнулись с трудностями и не знаете, что делать, задайте вопрос в нашем большом и дружном сообществе