7 키워드, 맵과 딕셔너리 - Keywords, maps and dicts

아직까지는 연관 데이터 구조에 대해 논의하지 않았습니다. 데이터구조는 키에 한개 또는 한개 이상의 값을 연관지을수 있습니다. 다른 언어에서는 다른말로 딕셔너리, 해쉬, 연관 배열, 맵, 기타등등으로 말하기도 합니다.

So far we haven't discussed any associative data structures, i.e. data structures that are able to associate a certain value (or multiple values) to a key. Different languages call these different names like dictionaries, hashes, associative arrays, maps, etc.

Elixir에는 키워드 리스트와 맵이라는 두 가지 주된 연관 데이터 구조가 있습니다. 이것들에 대해 배울 시간입니다!

In Elixir, we have two main associative data structures: keyword lists and maps. It's time to learn more about them!

7.1 키워드 리스트 - Keyword lists

많은 함수형 언어에서는 연관 데이터 구조의 표현으로 2-item 튜플을 가지는 리스트를 사용하는 것이 일반적입니다. Elixir는 첫 번째 요소가 원자형이고 튜플을 가지는 리스트의 경우 키워드 리스트라고 합니다:

In many functional programming languages, it is common to use a list of 2-item tuples as the representation of an associative data structure. In Elixir, when we have a list of tuples and the first item of the tuple (i.e. the key) is an atom, we call it a keyword list:

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
iex> list[:a]
1

위와 같이, Elixir는 이러한 리스트를 정의하는 데 특별한 문법을 지원하고 있으며, 뒤에서는 단순히 튜플을 가지는 리스트로 매핑하고 있습니다. 이것들은 그냥 리스트이므로, 리스트의 성능특징을 포함한 모든 가능한 operations들을 키워드 리스트에도 적용할 수 있습니다.

As you can see above, Elixir supports a special syntax for defining such lists, and underneath they just map to a list of tuples. Since they are simply lists, all operations available to lists, including their performance characteristics, also apply to keyword lists.

예를들어, 키워드 리스트에 새로운 값을 넣고 싶으면 ++ 를 사용할 수 있습니다.

For example, we can use ++ to add new values to a keyword list:

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

검색할때는 동일키값이 여러개 일때 제일 앞에 있는 값을 가져온다는 점에 주의하십시요:

Note that values added to the front are the ones fetched on lookup:

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

키워드 리스트는 2가지 특징때문에 중요합니다.

Keyword lists are important because they have two special characteristics:

  • 개발자가 지정한대로 키값이 정렬됩니다.
  • 한 개 이상의 동일 키값을 허용합니다.

  • They keep the keys ordered, as specified by the developer.

  • They allow a key to be given more than once.

예를들어, Ecto 라이브러리는 이러한 특징을 사용하여 데이터베이스 쿼리 작성을 위해 우아한 DSL(역자: Domain Specific Language)를 제공합니다.

For example, the Ecto library makes use of both features to provide an elegant DSL for writing database queries:

query = from w in Weather,
      where: w.prcp > 0,
      where: w.temp < 20,
     select: w

이 특징 때문에 키워드 리스트는 함수에 옵션값 전달을 위해 사용하는 기본 메커니즘이 됩니다. 5장에서 if/2 매크로에 대해 말할때 다음과 같은 구문을 지원했었습니다:

Those features are what prompted keyword lists to be the default mechanism for passing options to functions in Elixir. In chapter 5, when we discussed the if/2 macro, we mentioned the following syntax is supported:

iex> if false, do: :this, else: :that
:that

do:else:의 페어는 키워드 리스트입니다! 사실, 위의 코드는 아래와 동등합니다:

The do: and else: pairs are keyword lists! In fact, the call above is equivalent to:

iex> if(false, [do: :this, else: :that])
:that

일반적으로 함수의 마지막 인자가 키워드 리스트이면 대괄호는 써도 그만 안써도 그만입니다.

In general, when the keyword list is the last argument of a function, the square brackets are optional.

키워드 리스트를 조작하기 위해 Elixir는 the Keyword module을 제공합니다. 키워드 리스트는 단순히 리스트임을 명심하세요. 따라서 리스트와 같은 선형 성능 특징(긴 리스트에서 키를 찾는 것이나 item수를 찾는것 등등)을 제공합니다. 이러한 이유 때문에 키워드 리스트는 주로 옵션값으로 사용되고 있습니다. 만약 많은 item을 유지하거나 1개의 키가 1개의 값만 갖도록 guarantee 하고 싶을때는 맵을 사용해야합니다.

In order to manipulate keyword lists, Elixir provides the Keyword module. Remember though keyword lists are simply lists, and as such they provide the same linear performance characteristics as lists. The longer the list, the longer it will take to find a key, to count the number of items, and so on. For this reason, keyword lists are used in Elixir mainly as options. If you need to store many items or guarantee one-key associates with at maximum one-value, you should use maps instead.

키워드 리스트 또한 패턴 매칭이 가능합니다:

Note we can also pattern match on keyword lists:

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]

하여간 실전에서는 리스트의 item개수를 찾거나 리스트의 순서를 매칭시키기 위해 패턴 매칭을 쓸일은 거의 없습니다.

However this is rarely done in practice since pattern matching on lists require the number of items and their order to match.

7.2 맵 - Maps

키-값 저장이 필요할때마다 맵은 "go to" 데이터 구조입니다. 맵은 %{} 문법으로 생성됩니다:

Whenever you need a key-value store, maps are the "go to" data structure in Elixir. A map is created using the %{} syntax:

iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b

키워드 리스트에 비해 2가지 차이점을 이미 살펴봤습니다.

Compared to keyword lists, we can already see two differences:

  • 맵은 어떤 값이라도 키로 사용할 수 있습니다.
  • 맵의 키들은 어떤 순서도 따르지 않습니다.

  • Maps allow any value as a key.

  • Maps' keys do not follow any ordering.

만약 맵을 생성할때 중복 키를 넘기면 마지막것이 리턴됩니다:

If you pass duplicate keys when creating a map, the last one wins:

iex> %{1 => 1, 1 => 2}
%{1 => 2}

맵안의 코든 키들이 아톰형일때는 편리하게 키워드 문법을 사용할 수 있습니다:

When all the keys in a map are atoms, you can use the keyword syntax for convenience:

iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

키워드 리스트와는 대조적으로 맵은 패턴매칭과 함께쓸때 유용합니다:

In contrast to keyword lists, maps are very useful with pattern matching:

iex> %{} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

위와 같이, 맵은 주어진맵에 주어진 키가 존재하는한 매칭합니다. 그래서, 빈 맵도 모든 맵을 매칭합니다.

As shown above, a map matches as long as the given keys exist in the given map. Therefore, an empty map matches all maps.

맵에서 한가지 흥미로운 속성은 아톰형 키값을 업데이트하거나 accessing 하기 위해 특별 문법을 제공합니다.

One interesting property about maps is that they provide a particular syntax for updating and accessing atom keys:

iex> map = %{:a => 1, 2 => :b}
%{:a => 1, 2 => :b}
iex> map.a
1
iex> %{map | :a => 2}
%{:a => 2, 2 => :b}
iex> %{map | :c => 3}
** (ArgumentError) argument error

액세스와 업데이트 문법은 이미 존재하는 키에 대해서만 동작합니다. 예를들어, 마지막 코드는 맵에 :c가 없어서 실패했습니다. 이것은 맵을 다룰때 키가 있는 없는지 확인할 때 유용합니다.

Both access and update syntaxes above require the given keys to exist. For example, the last line failed because there is no :c in the map. This is very useful when you are working with maps where you only expect certain keys to exist.

다음 장에서는 컴파일시간을 보장하고 다형성 기초를 제공하는 구조체에 대해 배울것입니다. 구조체는 업데이트가 보장되는 맵 위에서 구현되어 매우 유용합니다.

In future chapters, we will also learn about structs, which provide compile-time guarantees and the foundation for polymorphism in Elixir. Structs are built on top of maps where the update guarantees above are proven to be quite useful.

맵을 조작하려면 the Map module을 사용합니다, API는 Keyword 모듈과 비슷합니다. 모두 Dict behaviour를 구현하고 있기 때문입니다.

Manipulating maps is done via the Map module, it provides a very similar API to the Keyword module. This is because both modules implement the Dict behaviour.

참고: 맵은 EEP 43구현을 통해 최근에 Erlang VM에 도입되었습니다. Erlang 17 버전은 EEP 구현의 일부인 "small maps" 만 지원합니다. 20~30개의 키를 maximum으로 저장할때만 성능이 좋다는걸 의미합니다. 이런 빈틈을 메꾸기 위해 Elixir는 수십만의 키에도 끄덕없는 the HashDict module도 제공합니다.

Note: Maps were recently introduced into the Erlang VM with EEP 43. Erlang 17 provides a partial implementation of the EEP, where only "small maps" are supported. This means maps have good performance characteristics only when storing at maximum a couple of dozens keys. To fill in this gap, Elixir also provides the HashDict module which uses a hashing algorithm to provide a dictionary that supports hundreds of thousands keys with good performance.

7.3 딕셔너리 - Dicts

Elixir에서는 키워드 리스트와 맵을 모두 딕셔너리라고 부릅니다. 다시 말해서 맵은 인터페이스의 일종 (Elixir에서는 behaviours 라고 부릅니다)이고 키워드 리스트와 맵 모듈은 이 인터페이스를 구현하고 있습니다.

In Elixir, both keyword lists and maps are called dictionaries. In other words, a dictionary is like an interface (we call them behaviours in Elixir) and both keyword lists and maps modules implement this interface.

the Dict module에 정의된 인터페이스는 내부 구현을 위임하는 API도 제공합니다.

This interface is defined in the the Dict module module which also provides an API that delegates to the underlying implementations:

iex> keyword = []
[]
iex> map = %{}
%{}
iex> Dict.put(keyword, :a, 1)
[a: 1]
iex> Dict.put(map, :a, 1)
%{a: 1}

Dict 모듈은 개발자가 Dict의 특징을 가진 own variation을 구현할 수 있고 기존 Elixir 코드를 후킹할 수 있게 해줍니다. Dict 모듈은 다른 딕셔너리들끼리 다루는 함수도 제공합니다. 예를들어 Dict.equal?/2는 어떤 종류의 2개의 딕셔너리도 비교할 수 있습니다.

The Dict module allows any developer to implement their own variation of Dict, with specific characteristics, and hook into existing Elixir code. The Dict module also provides functions that are meant to work across dictionaries. For example, Dict.equal?/2 can compare two dictionaries of any kind.

그렇다면 도대체 KeywordMap 또는 Dict 중에서 뭘 써야할까? 라는 고민이 생길 것입니다. 정답은 그때 그때 다릅니다.

that said, you may be wondering, which of Keyword, Map or Dict modules should you use in your code? The answer is: it depends.

만약 인자로 키워드를 사용하고 싶다면 Keyword 모듈을 사용하면됩니다. 만약 맵을 조작하고 싶다면 Map 모듈을 사용하면 되고 어떤한 딕셔너리를 다룰때는 Dict 모듈을 사용하면 됩니다.(그리고 다른 딕셔너리 구현체를 인자로 전달하는 테스트를 작성하는것도 잊지 마세요)

If your code is expecting a keyword as an argument, explicitly use the Keyword module. If you want to manipulate a map, use the Map module. However, if your API is meant to work with any dictionary, use the Dict module (and make sure to write tests that pass different dict implementations as arguments).

Elixir에서 연관 데이터 구조 설명은 여기서 마칩니다. 이제 연관 데이터 구조를 통해 문제를 해결하기 위해 키워드 리스트와 맵을 사용할 수 있을 것입니다.

This concludes our introduction to associative data structures in Elixir. You will find out that given keyword lists and maps, you will always have the right tool to tackle problems that require associative data structures in Elixir.