Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us

Built-In Elixir Modules

reading/built-in_modules.livemd

Built-In Elixir Modules

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue MeasurementsFilter Values By Type

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • Given you’re working with a particular data type such as an integer or map, where in the Elixir documentation should you look for relevant functionality?
  • How do you safely inspect any data type?
  • What are some common built-in functions developers use the most, and how do you use them?

Overview

Reinventing The Wheel

Rather than re-invent the wheel, we can rely upon already built tools to solve a range of common problems. Elixir provides built-in modules we can leverage to accomplish a wide variety of behavior without building our own custom solution.

Information Overload

The standard Elixir library contains a massive number of modules and functions. It’s not reasonable to expect your self to memorize all of these functions.

Instead, you should aspire to learn the most common modules and functions that you use everyday, and develop enough familiarity with the standard library to know where to look on HexDocs to find the appropriate tool when you need it.

Kernel

The Elixir Kernel module is part of the standard library in the Elixir programming language. It provides a number of basic functions for working with the underlying operating system, as well as functions that are core to Elixir.

Modules For Data Types

Most common data types have an associated module which contains functionality related to that data type.

For example:

Modules For Behavior

Elixir groups many modules into related behavior. For example, we’ve already seen the Enum module for data-agnostic enumeration.

The Kernel Module

The Kernel is the core of everything Elixir.

Even the operators you’ve already used are simply an alternative syntax to using functions in the Kernel.

Kernel.+(3, 3)

Kernel.elem/2

The Kernel.elem/2 function can retrieve an element from a tuple given an index.

Kernel.elem({3, 6, 9}, 0)

We can use Kernel functions with or without using the Kernel namespace.

elem({3, 6, 9}, 0)

Your Turn

Use Kernel.elem/2 to retrieve 100 from the following tuple.

Example solution

  tuple = {0, 4, 1, 100, 5, 7}
  elem(tuple, 3)
tuple = {0, 4, 1, 100, 5, 7}

Checking Types

Kernel contains many functions for determining a value’s type such as is_atom/1, is_binary/1, is_map/1, and is_integer/1.

true = Kernel.is_map(%{})
true = Kernel.is_atom(:an_atom)
true = Kernel.is_binary("")
true = Kernel.is_integer(1)

Your Turn

Use the Kernel module to check the types of each value in the cells below. You may have to read through the Kernel Documentation to find the appropriate functions.

Example solution

  is_atom(:example)
  is_map(%{})
  is_binary("")
  is_integer(1)
  is_float(1.0)
  is_boolean(true)
  is_list([])
  is_tuple({})

The first cell is filled out for sake of example. The result of each cell should be true.

is_atom(:example)
%{}
{}
[]
true
1.0
1
""

The Kernel is reasonably large. Remember, our goal is not to memorize every function but to develop familiarity with repeated practice.

Max And Min

We can use the max/2 and min/2 to return the largest (max) or smallest (min) value between two numbers.

max(100, 110)
min(100, 110)

Your Turn

We often use max/2 and min/2 to prevent a value from going above or below a value.

For example, let’s say we have a seconds variable that represents seconds on a clock. We want to prevent seconds from being less than 0 seconds, and more than 59 seconds.

Created a capped_seconds variable that uses the value of seconds and cannot go above 59 seconds or below 0 seconds.

Example solution

  seconds = 61
  capped_seconds = max(min(seconds, 59), 0)

Enter your solution below. if seconds is less than 0 capped_seconds should be 0. If seconds is greater than 59, then capped_seconds should be 59.

seconds = 60
capped_seconds = nil

Safe Inspection

Not all values can be interpolated inside of a string. Attempting to do so results in a protocol String.Chars not implemented error.

map = %{}
"#{map}"

String.Chars is a protocol for converting data types into a string. We’ll learn more about protocols in a future lesson. It’s enough for our purposes to know that this means the data type does not know how to be safely converted to a string.

We can use Kernel.inspect/2 to safely convert any Elixir term into a string.

inspect(%{})

Which allows us to use values that don’t implement the String.Chars protocol

map = %{}
"#{inspect(map)}"

Your Turn

Use Kernel.inspect/2 to safely interpolate the following variables inside of a string.

Example solution

  map = %{}
  list = [1, 2, 3]
  tuple = {1, 2, 3}

  "#{inspect(map)} #{inspect(list)} #{inspect(tuple)}"
map = %{}
list = [1, 2, 3]
tuple = {1, 2, 3}

""

The Integer Module

The Integer module contains functionality related to Integers.

We’ve already seen the Integer module in the Non-Enumerables reading material using Integer.digits/2.

Integer.digits(123_456_789)

As well as Integer.undigits/2.

Integer.undigits([1, 2, 3, 4, 5, 6, 7, 8, 9])

Parsing Integers From Strings

We can use Integer.parse/2 to parse an integer from a string. It returns a {integer, rest_of_string} tuple.

Integer.parse("2")

That makes it especially useful when we have a string with non-integer input such as a newline character \n.

user_input = "25\n"
Integer.parse(user_input)

Your Turn

Open the IEx shell by running iex in your command line. You must have Elixir installed locally for this to work.

$ iex

This should open the IEx Shell.

Erlang/OTP 25 [erts-13.0.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] [jit:ns]

Interactive Elixir (1.13.0) - press Ctrl+C to exit (type h() ENTER for help)

iex(1)>

You can use IO.gets/2 to retrieve user input in this environment (this does not work in Livebook because we do not have a Livebook console)

Use Integer.parse/1 to convert the string input into an integer.

iex(1)> IO.gets("Give me a number! ")
Give me a number! 25
"25\n"
iex(2)> IO.gets("Give me a number! ") |> Integer.parse()
Give me a number! 25
{25, "\n"}

Integer Math Functions

The Integer module includes several math-related functions such as Integer.gcd/2 (greatest common denominator), Integer.pow/2, Integer.mod/2, and others.

Likely, you’ll use the arithmetic operators and functions from the Kernel module and will rarely need to rely on these functions in the Integer module so we won’t go over them in greater detaill.

Your Turn

Use the Integer.gcd/2 function to determine the greatest common denominator of 10 and 15. The greatest common divisor (GCD) is the largest positive integer that divides both 10 and 15 evenly, so the result should be 5.

The String Module

The String module contains functionality related to strings.

Here are a few common functions to get you started.

Indexes

You can imagine a string like a list of characters. However, be warned this is only a mental model, strings are not actually implemented as lists they are implemented as binaries.

flowchart TB
  subgraph Characters
    S
    T
    R
    I
    N
    G
  end
  subgraph Indexes
    direction TB
    S --- 0
    T --- 1
    R --- 2
    I --- 3
    N --- 4
    G --- 5
  end

Notice that the index starts with 0, not 1, just like lists.

So the character at index 1 in "hello" would be "e".

flowchart TB
  subgraph Characters
    H
    E
    L1[L]
    L2[L]
    O
  end
  subgraph Indexes
    direction TB
    H --- 0
    E --- 1
    L1 --- 2
    L2 --- 3
    O --- 4
  end

Your Turn: String Exercises

Use the String.at/2 function to get the character at index 2 of "hello". The result should be the character l.

Example solution

  String.at("hello", 2)
"hello"

Use the String.at/2 function to retrieve the letter "o" in "hello"

Example solution

  String.at("hello", 4)
"hello"

Use the String.contains?/2 function to determine if "hello" contains "lo". The result should be true.

Example solution

  String.contains("hello", "lo")
"Hello"

Use String.capitalize/2 to capitalize "hello" to "Hello".

Example solution

  String.capitalize("hello")
"hello"

Use String.upcase/2 to upcase "hello" to "HELLO".

Example solution

  String.upcase("hello")
"hello"

Use String.downcase/2 to dowcase "HELLO" to "hello".

Example solution

  String.downcase("HELLO")
"HELLO"

Use String.split/3 to split the following comma-separated list of strings into a list of words.

Example solution

  String.split("have,a,great,day", ",")
"have,a,great,day"

Use String.trim/1 to remove whitespace from the " hello! " string.

Example solution

  String.trim("  hello!  ")
"  hello!  "

The List Module

The List module contains functionality related to lists.

Here are a few common functions to get you started.

  • List.delete_at/2 remove an element at an index in a list.
  • List.first/2 retrieve the first element in a list similar to hd/1 or pattern matching on the head and tail of a list.
  • List.flatten/2 flatten nested lists within a list.
  • List.insert_at/3 insert an element at a specified index within a list.
  • List.last/2 retrieve the last element in a list.
  • List.update_at/3 update an element at a specified index within a list.
  • List.zip/1 combine elements from multiple lists into a single list of tuples.

Mutation

Remember that in Elixir we do not mutate variables. That means the List.delete_at/2, List.insert_at/3 and List.update_at/3 functions do not mutate the original list. Instead, they create a new copy of the original list with the operation applied to it.

list = [1, 2, 3]
List.delete_at(list, 1)

Notice the original list variable has not changed.

list

Retrieving The First Element In A List

We have many different ways of retrieving the first element in a list. List.first/2 retrieves the first element in a list. We can also use Kernel.hd/1, Enum.at/2, or pattern matching.

head = List.first([1, 2, 3])
[head | _tail] = [1, 2, 3]
head
hd([1, 2, 3])
head = Enum.at([1, 2, 3], 0)

Try pattern matching, List.first/2, Enum.at/2, and hd/1 to retrieve the first element of the list below.

Your Turn: List Exercises

Use List.delete_at/2 to remove 2 from this list.

Example solution

  List.delete_at([2, 1, 3], 0)
[2, 1, 3]

Use List.flatten/1 to flatten the following list into [1, 2, 3, 4, 5, 6, 7, 8, 9]

Example solution

  List.flatten([1, 2, [3, 4, 5], 6, [7, [8, [9]]]])
[1, 2, [3, 4, 5], 6, [7, [8, [9]]]]

Use List.insert_at/3 to insert 2 into the following list to make [1, 2, 3].

Example solution

  List.insert_at([1, 3], 1, 2)
[1, 3]

Use List.last/2 to retrieve the last element 10000 in a list from 1 to 10000.

Example solution

  List.last(Enum.to_list(1..10000))

You might also use the piper operator |>.

  1..10000 |> Enum.to_list() |> List.last()
Enum.to_list(1..10000)

Use List.update_at/3 to subtract 2 from 4 in the following list to make [1, 2, 3].

Example solution

  List.update_at([1, 4, 3], 1, fn elem -> elem - 2 end)
[1, 4, 3]

Use List.zip/1 to combine these two lists to make [{"a", 1}, {"b", 2}, {"c", 3}].

Example solution

  letters = ["a", "b", "c"]
  numbers = [1, 2, 3]

  List.zip([letters, numbers])
letters = ["a", "b", "c"]
numbers = [1, 2, 3]

The Map Module

The Map module contains functionality related to maps.

Here are a few common functions to get you started.

Mutation

Once again, Map module functions do not mutate a value.

For example, if we use Map.put/3 to put a new value in a map, the original variable is not changed. Functions return a new value rather than modifying the original one.

original_map = %{}

new_map = Map.put(original_map, :key, "value")

So the original_map is still an empty map %{}.

original_map

And the new_map has been bound to the result of Map.put/3.

new_map

Your Turn: Map Exercises

Use Map.get/3 to retrieve the "world" value for the :hello key in the following map.

Example solution

  Map.get(%{hello: "world"}, :hello)
%{hello: "world"}

Use Map.put/3 to add the key :two with the value 2 to the following map.

Example solution

  Map.put(%{one: 1}, :two, 2)
%{one: 1}

Use Map.keys/1 to retrieve the keys for the following map.

Example solution

  Map.keys(%{key1: 1, key2: 2, key3: 3})
%{key1: 1, key2: 2, key3: 3}

Use Map.delete/2 to remove :key1 from the following map.

Example solution

  Map.delete(%{key1: 1, key2: 2, key3: 3}, :key1)
%{key1: 1, key2: 2, key3: 3}

Use Map.merge/2 to combine %{one: 1} and %{two: 2}.

Example Solution

  Map.merge(%{one: 1}, %{two: 2})

Use Map.update/4 or Map.update!/3 to update the :count key in this map to be 5 plus the existing value.

Example Solution

  Map.update(%{count: 10}, :count, 0, fn count -> count + 5 end)
%{count: 10}

Use Map.values/1 to retrieve the values [1, 2, 3] in the following map.

Example solution

  Map.values(%{key1: 1, key2: 2, key3: 3})
%{key1: 1, key2: 2, key3: 3}

The Keyword Module

The Keyword module contains functionality related to keyword lists.

Here are a few common functions to get you started.

Options

We often use keyword lists to provide optional arguments to a function.

For example, IO.inspect/2 has many optional arguments Including :label.

IO.inspect("world", label: "hello")

Under the hood, these functions may use Keyword.get/3 to retrieve optional arguments or provide default arguments.

It’s common to pass any number of options in a opts parameter, which should be the last parameter of the function.

defmodule MyIO do
  def inspect(value, opts \\ []) do
    label = Keyword.get(opts, :label, "default label")
    "#{label}: #{value}"
  end
end
MyIO.inspect("world")
MyIO.inspect("world", label: "hello")

Your Turn: Keyword Exercises

Use Keyword.get/3 to access the value for the :color key in the following keyword list.

Example solution

  Keyword.get([color: "red"], :color)
[color: "red"]

Use Keyword.get/3 to access the value for the :color key in the following empty list. If the :color key does not exist, provide a default value of "blue".

Example solution

  Keyword.get([], :color, "blue")
[]

Use the Keyword.keys/1 function to list all of the keys in the following keyword list.

Example solution

  Keyword.keys([one: 1, two: 2, three: 3])
[one: 1, two: 2, three: 3]

Use the Keyword.keyword?/1 function to determine if the following is a keyword list.

Example solution

  Keyword.keyword?([key: "value"])
[key: "value"]

Use the Keyword.keyword?/1 function to determine if an empty list is a keyword list.

Example solution

An empty list is technically a keyword list. That’s because there’s no difference between an empty keyword list and an empty list.

Technically, all of the elements in an empty list follow the {:atom, value} pattern that a keyword list enforces. Or perhaps it’s better to say that no elements violate the pattern.

  Keyword.keyword?([])
[]

Use the Keyword.keyword?/1 function to determine if the following list is a keyword list.

Example solution

A list is not a keyword list if it has any elements that don’t follow the {:atom, term} structure.

  Keyword.keyword?([1, 2, 3])
[1, 2, 3]

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

Commit Your Progress

DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.

Run git status to ensure there are no undesirable changes. Then run the following in your command line from the curriculum folder to commit your progress.

$ git add .
$ git commit -m "finish Built-In Elixir Modules reading"
$ git push

We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.

We also offer a paid course where you can learn from an instructor alongside a cohort of your peers. We will accept applications for the June-August 2023 cohort soon.

Navigation

Home Report An Issue MeasurementsFilter Values By Type