Learn With Me: Elixir - Keyword Lists (#19)

Keyword lists are another one of those data types that is actually implemented as something else. They look a lot like maps and they have a map-like interface, but they are actually implemented as lists of tuples.

The keyword list literals look like a list of key-value pairs.

Let's look at an exmaple.

iex> option_list = [size: 12, color: "red", color: "blue", style: :bold, style: :italic]
[size: 12, color: "red", color: "blue", style: :bold, style: :italic]

They in fact are key-value pairs. The key in a keyword list is always an atom and the value can be of any type. It looks a lot like a map, but it's enclosed in brackets, which indicate a list. It is actually a list of tuples, but with special syntax. To see what it looks like underneath, we'll prepend a non-tuple to the list.

iex> option_list = [nil | option_list]
[
  nil,
  {:size, 12},
  {:color, "red"},
  {:color, "blue"},
  {:style, :bold},
  {:style, :italic}
]

Since we added a nil, which does not fit in with the keyword list syntax, IEx now displays the keyword list as a normal list. As you can wee, all the key-value pairs are tuples in the list.

We can also construct a list of key-value tuples, and IEx will treat it like a keyword list.

iex> option_list = [{:hair, "orange"}, {:feet, :large}, {:height, 185}]
[hair: "orange", feet: :large, height: 185]

Why Keyword Lists?

So what's the point to a keyword list? We already have normal lists and we already have maps that store key-value pairs. The maps are even enumerable, so we have no problem iterating over the key-value pairs. I certainly thought this when I first saw keyword lists, and I didn't understand we why we would ever want a map with the performance characteristics of a list.

The primary benefit of keyword lists is that you can specify multiple key-value pairs for the same key. Also, the key-value pairs are ordered. Maps are unordered, but lists are ordered.

In Elixir keyword lists are typically used to specify options that are passed to a function. Often times, you'll be calling a function and you'll want to pass some options to modify how the function is supposed to behave. This is done in Elixir as a keyword list. It's a convention, so it's done even when there isn't a strict need for a keyword list.

Since a keyword list is a list, all the functions in the List and Enum modules can be also be used with a keyword list

Working With Keyword Lists

Keyword lists are typical of Elixir types in that the type that is accompanied by a module to allow you to manipulate the data, making it often unnecessary to know all the details regarding how the data type is implemented. In the case of keyword lists, functions used to manipulate keyword lists are located in the Keyword module. This module contains all sorts of useful functions to work with a keyword lists, such as functions to add, update, delete, replace, merge, etc.

Keyword List Example

Let's look at an example of a function that uses a keyword list for specifying options.

We can have a list of default options and then merge them with the options that are passed to the function. This allows us to only specify options that are different from the default options.

defmodule KeywordListExample do
	@default_options [color: "blue", size: "tiny", speed: "low"]
	
	def create_vehicle(registration_number, options \\ []) do
		#Merge the default options with the options that were passed
		options = Keyword.merge(@default_options, options)
		
		%{id: registration_number, color: options[:color], size: options[:size], speed: options[:speed]}
	end
end

This module has a function that creates a vehicle data structure. It accepts a registration number for the vehicle and a list of options. The caller can specify any number of options or none at all. Those options will override the default options, which is done using Keyword.merge/2, which merges the default options with the options passed into the function.

Let's look at some examples of calling the function.

iex> KeywordListExample.create_vehicle(4324324)
%{color: "blue", id: 4324324, size: "tiny", speed: "low"}
iex> KeywordListExample.create_vehicle(4324324, [color: "red"])
%{color: "red", id: 4324324, size: "tiny", speed: "low"}
iex> KeywordListExample.create_vehicle(4324324, [color: "red", color: "blue"])
%{color: "red", id: 4324324, size: "tiny", speed: "low"}
iex> KeywordListExample.create_vehicle(4324324, [color: "red", size: "medium", speed: "andante"])
%{color: "red", id: 4324324, size: "medium", speed: "andante"}

If no options are specified, the default options are applied. If some options are specified, those will be merged with the default options, with the options passed in taking precidence over the default options.

Since the options are a keyword list, I tried specifying multiple color options. Since the car created by this function only has one color, it just grabs the first color option and the other color option is ignored. If I wanted to have a car with multiple colors, I could have used Keyword.get_values(options, :color) to retrieve multiple color values.