До сих пор мы не говорили ни о каких ассоциативных структурах данных. Такие структуры позволяют ассоциировать некоторое значение (или несколько значений) с ключом.
В Эликсире есть два вида ассоциативных структур данных: ключевые списки и словари. Самое время познакомиться с ними!
Ключевые списки
Во многих функциональных языках программирования распространено использование массивов из кортежей, которые состоят из двух элементов, чтобы представить структуру из пар ключ/значение. В Эликсире, когда у нас есть список кортежей, и первый элемент кортежа (ключ) – это атом, мы называем это ключевым списком:
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
Как вы можете увидеть выше, Эликсир поддерживает специальный синтаксис для объявления таких списков: [key: value]
. Под капотом это интерпретируется как список кортежей выше. Т. к. ключевые списки – объекты типа List
, мы можем делать с ними все операции, доступные для списков. Например, можно добавить новое значение, используя ++
:
iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]
Обратите внимание, что при поиске будут возвращаться значения, стоящие ближе к началу списка:
iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0
Ключевые списки важны, потому что они имеют три особые характеристики:
- Ключи должны быть атомами.
- Ключи отсортированы так, как их задал разработчик.
- Ключи могут быть добавлены более одного раза.
Например, библиотека Экто использует эти особенности для предоставления элегантного DSL для написания запросов к базе данных:
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w
Эти характеристики являются причиной, по которой ключевые списки стали стандартным механизмом передачи опций в функции Эликсира. В главе «Конструкции ветвления», при обсуждении макроса if/2
мы упоминали, что поддерживается следующий синтаксис:
iex> if false, do: :this, else: :that
:that
Пары do:
и else:
– это ключевой список! Фактически, вызов выше соответствует этому:
iex> if(false, [do: :this, else: :that])
:that
Что, как мы уже знаем, представляет собой:
iex> if(false, [{:do, :this}, {:else, :that}])
:that
Когда ключевой список является последним аргументом функции, квадратные скобки не обязательны.
Хотя мы можем сопоставлять ключевые списки с образцом, это редко делается на практике, потому что предусматривает соответствие количества элементов и их порядка:
iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
Для манипуляций с ключевыми списками Эликсир предоставляет модуль Keyword
. Помните, что ключевые списки – это просто списки с такими же линейными характеристиками производительности. Чем длиннее список, тем дольше будет поиск по ключу, подсчёт количества элементов и т. д. По этой причине такие списки используются в Эликсире главным образом для передачи дополнительных значений. Если вам нужно хранить много элементов или исключить дублирование ключей, вам следует использовать словари.
Словари
Если вам нужно хранилище пар ключ/значение, словари – та структура, которую Эликсир предлагает в первую очередь. Словарь создаётся используя %{}
синтаксис:
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil
Сравнивая с ключевыми списками, мы уже можем увидеть два отличия:
- Словари допускают любое значение в качестве ключа.
- Ключи словарей не следуют какому-то определённому порядку.
В отличии от ключевых списков, словари очень удобны для сопоставления с образцом. Когда словарь используется в образце, он всегда будет соответствовать подмножеству сравниваемых значений:
iex> %{} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
Как показано выше, словарь соответствует до тех пор, пока ключи в образце существуют и в словаре справа. Также, пустой словарь будет соответствовать любому словарю.
Переменные могут быть использованы для доступа, сравнения и добавления ключей в словари:
iex> n = 1
1
iex> map = %{n => :one}
%{1 => :one}
iex> map[n]
:one
iex> %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
%{1 => :one, 2 => :two, 3 => :three}
Модуль Map
предоставляет API, очень похожее на модуль Keyword
, с удобными функциями для манипуляции со словарями:
iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(%{:a => 1, 2 => :b})
[{2, :b}, {:a, 1}]
Словари имеют следующий синтаксис для обновления значения для ключа:
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{map | 2 => "two"}
%{2 => "two", :a => 1}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
Синтаксис выше подразумевает, что ключ существует. Он не может быть использован для добавления новых ключей. Например, использование ключа :c
вызвало ошибку, потому что такого ключа нет в словаре.
Когда все ключи в словарее – это атомы, можно использовать синтаксис с использованием ключевых слов для удобства:
iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}
Другое интересное свойство словарей: у них есть свой собственный синтаксис для доступа к ключам-атомам:
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
При работе со словарями вместо использования функций из модуля Map
, эликсирщики обычно предпочитают синтаксис map.field
и сопоставление с образцом, которые приводят к ассертивному стилю программирования. В этом посте показаны преимущества и примеры, как писать более лаконичные и быстрые приложения при помощи написания ассертивного кода на Эликсире.
Обратите внимание. Словари в BEAM появились совсем недавно и только начиная с Эликсира версии
1.2
они способны эффективно хранить миллионы ключей. Если же вы работаете с предыдущими версиями Эликсира (1.0
или1.1
) и вам нужна поддержка хотя бы сотен ключей, вам может подойти модульHashDict
.
Вложенные структуры данных
Мы часто будем встречаться со словарями внутри словарей, или даже с ключевыми списками внутри словарей и т. п. Эликсир позаботился об удобстве манипуляций со вложенными структурами данных с помощью put_in/2
, update_in/2
и других макросов, использовать которые также удобно, как в императивных языках, сохраняя при этом иммутабельность.
Представьте, что у вас есть следующая структура:
iex> users = [
john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
[john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]
У нас есть ключевой список, в котором каждое значение – словарь с именем, возрастом и списком предпочитаемых языков программирования пользователей. Если мы хотим получить доступ к возрасту пользователя john
, мы можем написать:
iex> users[:john].age
27
Аналогичный синтаксис можно использовать для обновления значения:
iex> users = put_in users[:john].age, 31
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]
Макрос update_in/2
похож, но позволяет нам передать функцию, которая будет управлять изменением значения. Например, давайте уберём Clojure
из списка языков Mary
:
iex> users = update_in users[:mary].languages, fn languages -> List.delete(languages, "Clojure") end
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}]
Это далеко не вся информация о put_in/2
и update_in/2
, также есть макрос get_and_update_in/2
, который позволяет нам получить значение и одновременно с этим обновить структуру данных. Есть также put_in/3
, update_in/3
и get_and_update_in/3
, которые получают доступ в структуру данных динамически. Посмотрите их исчерпывающую документацию в модуле Kernel
.
На этом мы закончим введение в ассоциативные структуры данных Эликсира. В последствии вы обнаружите, что имея ключевые списки и словари, у вас всегда будет подходящий инструмент для решения проблем, в которых требуются ассоциативные структуры данных.