Для моделирования данных, которые лучше отражает предметную область, используют структуры.
Структуры по сути словари, но с некоторыми особенностями. Структуры проверяются на уровне компиляции, в них можно задать значения по умолчанию и структурам недоступны протоколы для словарей, например протоколы 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}
Обратите внимание, что структуры в модуле заранее определены.
Если вы зашли в тупик, то самое время задать вопрос в «Обсуждениях». Как правильно задать вопрос:
Тесты устроены таким образом, что они проверяют решение разными способами и на разных данных. Часто решение работает с одними входными данными, но не работает с другими. Чтобы разобраться с этим моментом, изучите вкладку «Тесты» и внимательно посмотрите на вывод ошибок, в котором есть подсказки.
Это нормально 🙆, в программировании одну задачу можно выполнить множеством способов. Если ваш код прошел проверку, то он соответствует условиям задачи.
В редких случаях бывает, что решение подогнано под тесты, но это видно сразу.
Создавать обучающие материалы, понятные для всех без исключения, довольно сложно. Мы очень стараемся, но всегда есть что улучшать. Если вы встретили материал, который вам непонятен, опишите проблему в «Обсуждениях». Идеально, если вы сформулируете непонятные моменты в виде вопросов. Обычно нам нужно несколько дней для внесения правок.
Кстати, вы тоже можете участвовать в улучшении курсов: внизу есть ссылка на исходный код уроков, который можно править прямо из браузера.
Ваше упражнение проверяется по этим тестам
1defmodule Test do
2 use ExUnit.Case
3
4 test "calculate_stats work" do
5 assert Solution.calculate_stats([]) == %{humans: 0, pets: 0}
6 assert Solution.calculate_stats([%User{}, %User{}, %Pet{}]) == %{humans: 2, pets: 1}
7 assert Solution.calculate_stats([%Pet{}, %Pet{}, %Pet{}]) == %{humans: 0, pets: 3}
8 end
9end
10
Решение учителя откроется через: