Ключевые списки и словари

До сих пор мы не говорили ни о каких ассоциативных структурах данных. Такие структуры позволяют ассоциировать некоторое значение (или несколько значений) с ключом.

В Эликсире есть два вида ассоциативных структур данных: ключевые списки и словари. Самое время познакомиться с ними!

Ключевые списки

Во многих функциональных языках программирования распространено использование массивов из кортежей, которые состоят из двух элементов, чтобы представить структуру из пар ключ/значение. В Эликсире, когда у нас есть список кортежей, и первый элемент кортежа (ключ) – это атом, мы называем это ключевым списком:

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.

На этом мы закончим введение в ассоциативные структуры данных Эликсира. В последствии вы обнаружите, что имея ключевые списки и словари, у вас всегда будет подходящий инструмент для решения проблем, в которых требуются ассоциативные структуры данных.

© 2020 / Россия Любые мысли и вопросы пишите на elixir@wunsh.ru.