Tuples and lists are powerful tools for creating complex data structures, but there are two key pieces missing from the story so far. Tuples are relatively anonymous structures. Relying on a specific order and number of components in tuples can create major maintenance headaches. Lists have similar problems: the usual approaches to list processing in Elixir assume that lists are just a sequence of (often) similar parts.
Sometimes you want to call things out by name instead of number, or pattern match to a specific location. Elixir has many different options for doing just that.
Maps and structs appeared late in Elixir’s development. They layer directly on features Erlang introduced in R17. In the long run, maps and structs will probably become the key pieces to know, but you may need the rest for compatibility with older Erlang code.
Sometimes you need to process lists of tuples containing two elements that can be considered as a “key and value” pair, where the key is an atom. Elixir displays them in keyword list format, and you may enter them in that format as well:
iex(1)>
planemo_list
=
[{
:earth
,
9.8
},
{
:moon
,
1.6
},
{
:mars
,
3.71
}]
[earth: 9.8, moon: 1.6, mars: 3.71]
iex(2)>
atomic_weights
=
[
hydrogen
:
1.008
,
carbon
:
12.011
,
sodium
:
22.99
]
[hydrogen: 1.008, carbon: 12.011, sodium: 22.99]
iex(3)>
ages
=
[
david
:
59
,
simon
:
40
,
cathy
:
28
,
simon
:
30
]
[david: 59, simon: 40, cathy: 28, simon: 30]
A keyword list is always sequential and can have duplicate keys. Elixir’s Keyword
module lets you access, delete, and insert values via their keys.
Use Keyword.get/3
to retrieve the first value in the list with a given key. The optional third argument to Keyword.get
provides a default value to return in case the key is not in the list. Keyword.fetch!/2
will raise an error if the key is not found. The Keyword.get_values/2
will return all the values for a given key:
iex(5)>
Keyword
.
get
(
atomic_weights
,
:hydrogen
)
1.008
iex(6)>
Keyword
.
get
(
atomic_weights
,
:neon
)
nil
iex(7)>
Keyword
.
get
(
atomic_weights
,
:carbon
,
0
)
12.011
iex(8)>
Keyword
.
get
(
atomic_weights
,
:neon
,
0
)
0
iex(9)>
Keyword
.
fetch!
(
atomic_weights
,
:neon
)
** (KeyError) key :neon not found in: [hydrogen: 1.008, carbon: 12.011, sodium: 22.99]
(elixir) lib/keyword.ex:312: Keyword.fetch!/2
iex(10)>
Keyword
.
get_values
(
ages
,
:simon
)
[40,30]
You can use Keyword.has_key?/2
to see if a key exists in the list:
iex(11)>
Keyword
.
has_key?
(
atomic_weights
,
:carbon
)
true
iex(12)>
Keyword
.
has_key?
(
atomic_weights
,
:neon
)
false
To add a new value, use Keyword.put_new/3
. If the key already exists, its value remains unchanged:
iex(13)>
weights2
=
Keyword
.
put_new
(
atomic_weights
,
:helium
,
4.0026
)
[helium: 4.0026, hydrogen: 1.008, carbon: 12.011, sodium: 15.999]
iex(14)>
weights3
=
Keyword
.
put_new
(
weights2
,
:helium
,
-
1
)
[helium: 4.0026, hydrogen: 1.008, carbon: 12.011, sodium: 22.99]
To replace a value, use Keyword.put/3
If the key doesn’t exist, it will be created. If it does exist, all entries for that key will be removed and the new entry added:
iex(15)>
ages2
=
Keyword
.
put
(
ages
,
:chung
,
19
)
[chung: 19, david: 59, simon: 40, cathy: 28, simon: 30]
iex(16)>
ages3
=
Keyword
.
put
(
ages2
,
:simon
,
22
)
[simon: 22, chung: 19, david: 59, cathy: 28]
All of these functions are copying lists or creating new modified versions of a list. As you’d expect in Elixir, the original list remains untouched.
If you want to delete all entries for a key, use Keyword.delete/2
; to delete only the first entry for a key, use Keyword.delete_first/2
:
iex(17)>
ages2
[chung: 19, david: 59, simon: 40, cathy: 28, simon: 30]
iex(18)>
ages4
=
Keyword
.
delete
(
ages2
,
:simon
)
[chung: 19, david: 59, cathy: 28]
If you had created the list of atomic weights with tuples that included both the element name and its chemical symbol, you could use either the first or second element in the tuple as a key:
iex(1)>
atomic_info
=
[{
:hydrogen
,
:H
,
1.008
},
{
:carbon
,
:C
,
12.011
},
...(1)>
{
:sodium
,
:Na
,
22.99
}]
[{:hydrogen, :H, 1.008}, {:carbon, :C, 12.011}, {:sodium, :Na, 22.99}]
If you have data structured this way, you can use the List.keyfind/4
, List.keymember?/3
, List.keyreplace/4
, List.keystore/4
, and List.keydelete/3
functions to manipulate the list. Each of these functions takes the list as its first argument. The second argument is the key you want to find, and the third argument is the position within the tuple that should be used as the key, with 0
as the first element:
iex(1)>
atomic_info
=
[{
:hydrogen
,
:H
,
1.008
},
{
:carbon
,
:C
,
12.011
},
...(1)>
{
:sodium
,
Na
,
22.99
}]
[{:hydrogen, :H, 1008}, {:carbon, :C, 12.011}, {:sodium, Na, 22.99}]
iex(2)>
List
.
keyfind
(
atomic_info
,
:H
,
1
)
{:hydrogen, :H, 1.008}
iex(3)>
List
.
keyfind
(
atomic_info
,
:carbon
,
0
)
{:carbon, :C, 12.011}
iex(4)>
List
.
keyfind
(
atomic_info
,
:F
,
1
)
nil
iex(5)>
List
.
keyfind
(
atomic_info
,
:fluorine
,
0
,
{})
{}
iex(6)>
List
.
keymember?
(
atomic_info
,
:Na
,
1
)
true
iex(7)>
List
.
keymember?
(
atomic_info
,
:boron
,
0
)
false
iex(8)>
atomic_info2
=
List
.
keystore
(
atomic_info
,
:boron
,
0
,
...(8)>
{
:boron
,
:B
,
10.081
})
[{:hydrogen, :H, 1008}, {:carbon, :C, 12.011}, {:sodium, Na, 22.99},
{:boron, :B, 10.081}]
iex(9)>
atomic_info3
=
List
.
keyreplace
(
atomic_info2
,
:B
,
1
,
...(9)>
{
:boron
,
:B
,
10.81
})
[{:hydrogen, :H, 1008}, {:carbon, :C, 12.011}, {:sodium, Na, 22.99},
{:boron, :B, 10.81}]
iex(10)>
atomic_info4
=
List
.
keydelete
(
atomic_info3
,
:fluorine
,
0
)
[{:hydrogen, :H, 1008}, {:carbon, :C, 12.011}, {:sodium, Na, 22.99},
{:boron, :B, 10.81}]
iex(11)>
atomic_info5
=
List
.
keydelete
(
atomic_info3
,
:carbon
,
0
)
[{:hydrogen, :H, 1008}, {:sodium, Na, 22.99}, {:boron, :B, 10.81}]
Lines 2 and 3 show that you can search the list by chemical name (position 0) or symbol (position 1). By default, trying to find a key that doesn’t exist returns nil
(line 4), but you may return any value you choose (line 5). Lines 6 and 7 show the use of List.keymember?
.
To add new values, you must give a complete tuple as the last argument, as shown in line 8. The value for the atomic weight of boron was deliberately entered incorrectly. Line 9 uses List.keyreplace
to correct the error.
You can also use List.keyreplace
to replace the entire tuple. If you wanted to replace boron with zinc, you would have typed:
iex(9)>
atomic_info3
=
List
.
keyreplace
(
atomic_info2
,
:B
,
1
,
{
:zinc
,
:Zn
,
65.38})
Lines 10 and 11 show what happens when you use List.keydelete
on an entry that is not in the list and on one that is in the list.
If you know that your keys will be unique, you can create a hash dictionary (HashDict
), which is an associative array. Hash dictionaries aren’t really lists, but I am including them in this chapter because all of the functions that you have used with a Keyword
list will work equally well with a HashDict
. The advantage of a HashDict
over a Keyword
list is that it works well for large amounts of data. In order to use a hash dictionary, you must explicitly create it with the HashDict.new
function:
iex(1)>planemo_hash = Enum.into([earth: 9.8, moon: 1.6, mars: 3.71],
HashDict.new())
#HashDict<[earth: 9.8, mars: 3.71, moon: 1.6]>
iex(2)>
HashDict
.
has_key?
(
planemo_hash
,
:moon
)
true
iex(3)>
HashDict
.
has_key?
(
planemo_hash
,
:jupiter
)
false
iex(4)>
HashDict
.
get
(
planemo_hash
,
:jupiter
)
nil
iex(5)>
HashDict
.
get
(
planemo_hash
,
:jupiter
,
0
)
0
iex(6)>
planemo_hash2
=
HashDict
.
put_new
(
planemo_hash
,
:jupiter
,
99.9
)
#HashDict<[moon: 1.6, mars: 3.71, jupiter: 99.9, earth: 9.8]>
iex(7)>
planemo_hash3
=
HashDict
.
put_new
(
planemo_hash2
,
:jupiter
,
23.1
)
#HashDict<[moon: 1.6, mars: 3.71, jupiter: 99.9, earth: 9.8]>
iex(8)>
planemo_hash4
=
HashDict
.
put
(
planemo_hash3
,
:jupiter
,
23.1
)
#HashDict<[moon: 1.6, mars: 3.71, jupiter: 23.1, earth: 9.8]>
iex(9)>
planemo_hash5
=
HashDict
.
delete
(
planemo_hash4
,
:saturn
)
#HashDict<[moon: 1.6, mars: 3.71, jupiter: 23.1, earth: 9.8]>
iex(10)>
planemo_hash6
=
HashDict
.
delete
(
planemo_hash4
,
:jupiter
)
#HashDict<[moon: 1.6, mars: 3.71, earth: 9.8]>
Line 6 deliberately sets Jupiter’s gravity to an incorrect value. Line 7 shows that HashDict.put_new/2
will not update an existing value; line 8 shows that HashDict.put
will update existing values. Line 9 shows that attempting to delete a nonexistent key from a hash dictionary leaves it unchanged.
Keyword lists are a convenient way to address content stored in lists by key, but underneath, Elixir is still walking through the list. That might be OK if you have other plans for that list requiring walking through all of it, but it can be unnecessary overhead if you’re planning to use keys as your only approach to the data.
The Erlang community, after dealing with these issues for years, added a new set of tools, maps, to R17. (The initial implementation is partial but will get you started.) Elixir simultaneously added support for the new feature, with, of course, a distinctive Elixir syntax.
The simplest way to create a map is to use %{}
to create an empty map:
iex(1)>
new_map
=
%{}
%{}
Frequently, you’ll want to create maps with at least some initial values. Elixir offers two ways to do this. You use the same %{}
syntax, but put some extra declarations inside:
iex(2)>
planemo_map
=
%{
:earth
=>
9.8
,
:moon
=>
1.6
,
:mars
=>
3.71
}
%{earth: 9.8, mars: 3.71, moon: 1.6}
The map now has keys that are the atoms :earth
, :moon
, and :mars
, pointing to the values 9.8, 1.6, and 3.71, respectively. The nice thing about this syntax is that you can use any kind of value as the key. It’s perfectly fine, for example, to use numbers for keys:
iex(3)>
number_map
=
%{
2
=>
"two"
,
3
=>
"three"
}
%{2 => "two", 3 => "three"}
However, atoms are probably the most common keys, and Elixir offers a more concise syntax for creating maps that use atoms as keys:
iex(4)>
planemo_map_alt
=
%{
earth
:
9.8
,
moon
:
1.6
,
mars
:
3.71
}
%{earth: 9.8, mars: 3.71, moon: 1.6}
The responses created by IEx in response to lines 2 and 4 are identical, and Elixir itself will use the more concise syntax if appropriate.
If the strength of a planemo’s gravitational field changes, you can easily fix that with:
iex(7)>
altered_planemo_map
=
%{
planemo_map
|
earth
:
12
}
%{earth: 12, mars: 3.71, moon: 1.6}
or:
iex(8)>
altered_planemo_map
=
%{
planemo_map
|
:earth
=>
12
}
%{earth: 12, mars: 3.71, moon: 1.6}
You can update multiple key-value pairs if you want, with syntax like %{planemo_map | earth: 12, mars:3}
or %{planemo_map | :earth => 12, :mars => 3}
.
You may also want to add another key-value pair to a map. You can’t, of course, change the map itself, but the Dict.put_new
library function can easily create a new map that includes the original plus an extra value:
iex(7)>
extended_planemo_map
=
Dict
.
put_new
(
planemo_map
,
:jupiter
,
23.1
)
%{earth: 9.8, jupiter: 23.1, mars: 3.71, moon: 1.6}
The Dict
library lets you treat maps much like HashDict
if you want that style of access, too.
Elixir lets you extract information from maps through pattern matching. The same syntax works whether you’re matching in a variable line or in a function clause. Need the gravity for earth?
iex(12)>
%{
earth
:
earth_gravity
}
=
planemo_map
%{earth: 9.8, mars: 3.71, moon: 1.6}
iex(13)>
earth_gravity
9.8
If you ask for a value from a key that doesn’t exist, you’ll get an error. (If you need to pattern match “any map,” just use the empty map, %{}
.)
One shortcoming of tuples, keyword lists, and maps is that they are fairly unstructured. When you use tuples, you are responsible for remembering the order in which the data items occur in the tuple. With keyword lists and maps, you can add a new key at any time or misspell a key name, and Elixir will not complain. Elixir structs overcome these problems. They are based on maps, so the order of key-value pairs doesn’t matter, but a struct also keeps track of the key names and makes sure you don’t use invalid keys.
Using structs requires telling Elixir about them with a special declaration. You use a defstruct
declaration (actually a macro, as you’ll see later) inside of a defmodule
declaration:
defmodule
Planemo
do
defstruct
name
:
:nil
,
gravity
:
0
,
diameter
:
0
,
distance_from_sun
:
0
end
That defines a struct named Planemo
, containing fields named name
, gravity
, diameter
, and distance_from_sun
with their default values. This declaration creates structs for different towers for dropping objects:
defmodule
Tower
do
defstruct
location
:
""
,
height
:
20
,
planemo
:
:earth
,
name
:
""
end
Find these in ch07/ex1-struct, compile them in IEx, and you can start using the structs to store data. As you can see on line 3, creating a new struct with empty {}
applies the default values, while specifying values as shown on line 4 overrides the defaults:
iex -S mix
Erlang/OTP 19 [erts-8.0] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe]
[kernel-poll:false]
Compiling 2 files (.ex)
Generated tower app
Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>
tower1
=
%
Tower
{}
%Tower{height: 20, location: "", name: "", planemo: :earth}
iex(2)>
tower2
=
%
Tower
{
location
:
"Grand Canyon"
}
%Tower{height: 20, location: "Grand Canyon", name: "", planemo: :earth}
iex(3)>
tower3
=
%
Tower
{
location
:
"NYC"
,
height
:
241
,
name
:
"Woolworth Building"
}
%Tower{height: 241, location: "NYC", name: "Woolworth Building",
planemo: :earth}
iex(4)>
tower4
=
%
Tower
{
location
:
"Rupes Altat 241"
,
height
:
500
,
...(4)>
planemo
:
:moon
,
name
:
"Piccolini View"
}
%Tower{height: 500, location: "Rupes Altat 241", name: "Piccolini View",
planemo: :moon}
iex(5)>
tower5
=
%
Tower
{
planemo
:
:mars
,
height
:
500
,
...(5)>
name
:
"Daga Vallis"
,
location
:
"Valles Marineris"
}
%Tower{height: 500, location: "Valles Marineris", name: "Daga Vallis",
planemo: :mars}
iex(6)>
tower5
.
name
"Daga Vallis"
These towers (or at least drop sites) demonstrate a variety of ways to use the record syntax to create variables as well as interactions with the default values:
Line 1 just creates tower1
with the default values. You can add real values later.
Line 2 creates a tower2
with a location
, but otherwise relies on the default values.
Line 3 overrides the default values for location
, height
, and name
, but leaves the planemo
alone.
Line 4 overrides all of the default values.
Line 5 replaces all of the default values, and also demonstrates that it doesn’t matter in what order you list the name/value pairs. Elixir will sort it out.
Once you have values in your structs, you can extract the values using the dot notation shown on line 6, which may be familiar from other programming languages.
Since structures are maps, pattern matches against structures work in exactly the same way as they do for maps.
iex(7)>
%
Tower
{
planemo
:
p
,
location
:
where
}
=
tower5
%Tower{height: 500, location: "Valles Marineris", name: "Daga Vallis",
planemo: :mars}
iex(8)>
p
:mars
iex(9)>
where
"Valles Marineris"
You can pattern match against structures submitted as arguments. The simplest way to do this is to just match against the record, as shown in Example 7-1, which is in ch07/ex2-struct-match.
defmodule
StructDrop
do
def
fall_velocity
(
t
)
do
fall_velocity
(
t
.
planemo
,
t
.
height
)
end
def
fall_velocity
(
:earth
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
9.8
*
distance
)
end
def
fall_velocity
(
:moon
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
1.6
*
distance
)
end
def
fall_velocity
(
:mars
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
3.71
*
distance
)
end
end
This uses a pattern match that will match only Tower
records, and puts the record into a variable t
. Then, like its predecessor in Example 3-8, it passes the individual arguments to fall_velocity/2
for calculations, this time using the record syntax:
iex(13)>
r
(
StructDrop
)
warning: redefining module StructDrop (current version loaded from
_build/dev/lib/struct_drop/ebin/Elixir.StructDrop.beam)
lib/struct_drop.ex:1
{:reloaded, StructDrop, [StructDrop]}
[StructDrop]
iex(14)>
StructDrop
.
fall_velocity
(
tower5
)
60.909769331364245
iex(15)>
StructDrop
.
fall_velocity
(
tower1
)
19.79898987322333
The StructDrop.fall_velocity/1
function shown in Example 7-2 pulls out the planemo
field and binds it to the variable planemo
. It pulls out the height
field and binds it to distance
. Then it returns the velocity of an object dropped from that distance
just like earlier examples throughout this book.
You can also extract the specific fields from the structure in the pattern match, as shown in Example 7-2, which is in ch07/ex3-struct-components.
defmodule
StructDrop
do
def
fall_velocity
(%
Tower
{
planemo
:
planemo
,
height
:
distance
})
do
fall_velocity
(
planemo
,
distance
)
end
def
fall_velocity
(
:earth
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
9.8
*
distance
)
end
def
fall_velocity
(
:moon
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
1.6
*
distance
)
end
def
fall_velocity
(
:mars
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
3.71
*
distance
)
end
end
You can take the Tower
structures you have created and feed them into this function, and it will tell you the velocity resulting from a drop from the top of that tower to the bottom.
Finally, you can pattern match against both the fields and the structure as a whole. Example 7-3, in ch07/ex4-struct-multi, demonstrates using this mixed approach to create a more detailed response than just the fall velocity.
defmodule
StructDrop
do
def
fall_velocity
(
t
=
%
Tower
{
planemo
:
planemo
,
height
:
distance
})
do
IO
.
puts
(
"From
#{
t
.
name
}
's elevation of
#{
distance
}
meters on
#{
planemo
}
,"
)
IO
.
puts
(
"the object will reach
#{
fall_velocity
(
planemo
,
distance
)
}
m/s"
)
IO
.
puts
(
"before crashing in
#{
t
.
location
}
"
)
end
def
fall_velocity
(
:earth
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
9.8
*
distance
)
end
def
fall_velocity
(
:moon
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
1.6
*
distance
)
end
def
fall_velocity
(
:mars
,
distance
)
when
distance
>=
0
do
:math
.
sqrt
(
2
*
3.71
*
distance
)
end
end
It is possible to have a variable whose name is the same as a field name; in the previous example, the planemo
field was assigned to a variable also named planemo
.
If you pass a Tower
structure to StructDrop.fall_velocity/1
, it will match against individual fields it needs to do the calculation and match the whole record into t
so that it can produce a more interesting if not necessarily grammatically correct report:
iex(16)>
StructDrop
.
fall_velocity
(
tower5
)
From Daga Vallis's elevation of 500 meters on mars,
the object will reach 60.90976933136424520399 m/s
before crashing in Valles Marineris
:ok
iex(17)>
StructDrop
.
fall_velocity
(
tower3
)
From Woolworth Building's elevation of 241 meters on earth,
the object will reach 68.72845116834803036454 m/s
before crashing in NYC
:ok
Elixir lets you attach behavior to structures (and, in fact, any type of data) with protocols. For example, you may want to test to see if a structure is valid or not. Clearly, the test for what is a valid structure varies from one type of structure to another. For example, you may consider a Planemo
valid if its gravity, diameter, and distance from the sun are nonnegative. A Tower
is valid if its height is nonnegative and it has a non-nil
value for a planemo
.
Example 7-4 shows the definition of a protocol for testing validity. The files for this example are in ch07/ex5-protocol.
defprotocol
Valid
do
@doc
"Returns true if data is considered nominally valid"
def
valid?
(
data
)
end
The interesting line here is the def valid?(data)
; it is, in essence, an incomplete function definition. Every data type whose validity you want to test will have to provide a complete function with the name valid?
, so let’s add some code to the definition of the Planemo
structure:
defmodule
Planemo
do
defstruct
name
:
:nil
,
gravity
:
0
,
diameter
:
0
,
distance_from_sun
:
0
end
defimpl
Valid
,
for
:
Planemo
do
def
valid?
(
p
)
do
p
.
gravity
>=
0
&&
p
.
diameter
>=
0
&&
p
.
distance_from_sun
>=
0
end
end
Let’s test that out right now. Some of the output lines have been split for ease of reading:
iex(1)>
p
=
%
Planemo
{}
%Planemo{diameter: 0, distance_from_sun: 0, gravity: 0, name: nil}
iex(2)>
Valid
.
valid?
(
p
)
true
iex(3)>
p2
=
%
Planemo
{
name
:
:weirdworld
,
gravity
:
-
2.3
}
%Planemo{diameter: 0, distance_from_sun: 0, gravity: -2.3, name: :weirdworld}
iex(4)>
Valid
.
valid?
(
p2
)
false
iex(5)>
t
=
%
Tower
{}
%Tower{height: 20, location: "", name: "", planemo: :earth}
iex(6)>
Valid
.
valid?
(
t
)
** (Protocol.UndefinedError) protocol Valid not implemented for
%Tower{height: 20, location: "", name: "", planemo: :earth}
valid_protocol.ex:1: Valid.impl_for!/1
valid_protocol.ex:3: Valid.valid?/1
Lines 1 and 2 show the creation and testing of a valid Planemo
; lines 3 and 4 show the results for an invalid one. Line 6 shows that you cannot test a Tower
structure for validity yet, as the valid?
function has not yet been implemented. Here is the updated code for the Tower
, which you can find in ch07/ex6-protocol:
defmodule
Tower
do
defstruct
location
:
""
,
height
:
20
,
planemo
:
:earth
,
name
:
""
end
defimpl
Valid
,
for
:
Tower
do
def
valid?
(%
Tower
{
height
:
h
,
planemo
:
p
})
do
h
>=
0
&&
p
!=
nil
end
end
Here is the test:
iex(7)>
r
(
Tower
)
warning: redefining module Tower (current version loaded from
_build/dev/lib/valid/ebin/Elixir.Tower.beam)
lib/tower.ex:1
{:reloaded, Tower, [Tower]}
iex(8)>
Valid
.
valid?
(
t
)
true
iex(12)>
t2
=
%
Tower
{
height
:
-
2
,
location
:
"underground"
}
%Tower{height: -2, location: "underground", name: "", planemo: :earth}
iex(13)>
Valid
.
valid?
(
t2
)
false
When you inspect
a Tower
, you get rather generic output:
iex(1)>
t3
=
%
Tower
{
location
:
"NYC"
,
height
:
241
,
name
:
"Woolworth Building"
}
%Tower{height: 241, location: "NYC", name: "Woolworth Building",
planemo: :earth}
iex(2)>
inspect
t3
"%Tower{height: 241, location: "NYC", name: "Woolworth Building", planemo: :earth}"
Wouldn’t it be nice to have better-looking output? You can do this by implementing the Inspect
protocol for Tower
structures. Example 7-5 shows the code to add to tower.ex; you will find the source in ch07/ex7-inspect:
defimpl
Inspect
,
for
:
Tower
do
import
Inspect.Algebra
def
inspect
(
item
,
_options
)
do
metres
=
concat
(
to_string
(
item
.
height
),
"m:"
)
msg
=
concat
([
metres
,
break
,
item
.
name
,
","
,
break
,
item
.
location
,
","
,
break
,
to_string
(
item
.
planemo
)])
end
end
The Inspect.Algebra
module implements “pretty printing” using an algebraic approach (hence the name). In the simplest form, it puts together documents that may be separated by optional line breaks (break
) and connected with concat
. Every place that you put a break
in a document is replaced by a space, or, if there is not enough space on the line, a line break.
The inspect/2
function takes the item you want to inspect as its first argument. The second argument is a structure that lets you specify options that give you greater control on how inspect/2
produces its output.
The first concat
puts the height and abbreviation for metres together without any intervening space. The second concat
connects all the items in the list, so the function returns a string containing the pretty-printed document. Since Valid
is a protocol rather than a module, we have to compile the
file:
iex(3)>
c
(
"lib/valid.ex"
)
warning: redefining module Valid (current version loaded from
_build/dev/consolidated/Elixir.Valid.beam)
lib/valid.ex:1
[Valid]
iex(4)>
inspect
t3
"241m: Woolworth Building, NYC, earth"
3.15.168.214