Skip to content

Latest commit

 

History

History
1251 lines (960 loc) · 30.8 KB

notebook.livemd

File metadata and controls

1251 lines (960 loc) · 30.8 KB

Elixir Training

Basics - types

What we'll cover in this section includes:

  • Strings and Atoms
  • Booleans
  • Comparison operators
  • Lists and tuples
  • Maps and structs
  • Keyword lists

Strings and Atoms

https://hexdocs.pm/elixir/String.html https://hexdocs.pm/elixir/Atom.html

"We like strings! They are utf-8 encoded!"
'We only use charlists when working with Erlang libraries! They are just lists of integers!'
name = "Tim"
"We can do string interpolation #{name}"

# Atoms are like symbols in other languages
:atoms_are_not_garbage_collected
# Use atoms for distinct values
:ok
:error
:"stage complete"

"we love " <> "concatenating things"
'we love ' ++ 'concatenating things'
# Some examples to understand why we might prefer strings to charlists

# --> 5 (counts graphmemes)
String.length("héllo")

# --> 6 (counts code points - é is represented by 2 code points)
length('héllo')
QUESTION!
When parsing user input is it better to parse to strings, charlists, or atoms? Why?

Booleans

# and, or --> both require boolean values as inputs
# --> false
true and false
# --> true
true or false
# --> error!!
# 1 or true

# &&, || --> can handle truthy/falsy values. 
# nil and false are the only falsy values
# --> 1
1 || true
# --> true
true || 1
# --> nil
1 && nil

# Some of the stranger cases...
# --> 1
false or 1
# --> error!!
# 1 or false

Comparisons

# --> true
"a" == "a"
# --> true 
1 == 1.0
# --> false 
1 === 1.0
# --> true 
5.0 <= 10
# --> true. All types can be compared. There is an overall ordering of types:
1 < :atom
# number < atom < reference < function < port < pid < tuple < map < list < bitstring

Lists and Tuples

  • Lists are linked lists (each element has a pointer to the next)
    • they can contain any, mixed values, atoms, strings, maps or even other lists.
    • Prepending elements is fast
    • Looking up an element or getting the length of a list takes time proportional to the list length
    • The Enum module contains useful methods for lists, maps, and ranges
    • The Stream module contains similar methods, but the lazy equivalents
    • The built-in List module has some helpful util methods
  • Streams are lazy lists, and can be infinite
  • Tuples are stored contiguously in memory
    • Prepending elements or editing is slow (requires the whole tuple to be copied)
    • Looking up an element or getting the length is constant in time (fast!)
    • The built-in Tuple module has some helpful util methods - https://hexdocs.pm/elixir/Tuple.html
# A list
[1, 2, 3]
# --> [1, 2, 3] - Syntax for prepending to list
[1 | [2, 3]]
# --> [1, 2, 3, 4]
[1, 2] ++ [3, 4]
# A tuple
tuple = {:ok, 15, "humans"}
QUESTION!
I want a data structure to store a users name, hair colour, and height - would lists or tuples be more appropriate?
I want a data structure to store the messages a user has sent - would lists or tuples be more appropriate?

Interlude - the pipe operator!!

The pipe operator is a handy way to pipe the output of a function into another function. This helps avoid lots of nested functions.

defmodule Pipe do
  def add_1(x), do: x + 1
  def times_3(x), do: x * 3
  def minus_n(x, n), do: x - n
end

x = 5

# If I want to add 1, then times 3 without a pipe I need to do something like this:
Pipe.times_3(Pipe.add_1(x))
# Not too bad, but if you have a lot of chained function calls it can get quite cumbersome

# Instead we can use the pipe operator
x
|> Pipe.add_1()
|> Pipe.times_3()

# If we pipe to a function which takes multiple arguments the piped value is used at the first 
#  argument
x
|> Pipe.add_1()
|> Pipe.times_3()
|> Pipe.minus_n(4)

# Is the same as...
Pipe.minus_n(x, 4)

Some examples with the Enum and Stream module

  • Both modules provide similar methods, but the Stream module has operations which are lazy
# This code doesn't output a list! Just a description of transformations we want to do
[1, 2, 3]
|> Stream.map(fn x -> x + 1 end)
|> Stream.map(fn x -> x + 3 end)
QUESTION!
What output would you expect from the following code and why?
nums = [0, 10, 20]

defmodule Add do
  def add_one(x) do
    IO.puts(x)
    x + 1
  end

  def enum_fn(nums) do
    nums
    |> Enum.map(&Add.add_one/1)
    |> Enum.map(&Add.add_one/1)
    |> Enum.map(&Add.add_one/1)
  end

  def stream_fn(nums) do
    nums
    |> Stream.map(&Add.add_one/1)
    |> Stream.map(&Add.add_one/1)
    |> Stream.map(&Add.add_one/1)
    |> Enum.to_list()
  end
end

IO.puts("Running Enum version")
Add.enum_fn(nums)
IO.puts("Running Stream version")
Add.stream_fn(nums)

Maps and Structs

Maps Intro

map = %{
  "my_key" => "my_value",
  57 => %{"nested_map_key" => "nested_value"},
  :atoms_make_good_keys => "yay!",
  :tricky_case => nil
}

# What are the outputs from each of the following?
Map.get(map, "my_key") |> IO.inspect(label: "my_key")
Map.get(map, "non-existent") |> IO.inspect(label: "non-existent")
Map.get(map, "non-existent-defaulted", :my_default) |> IO.inspect(label: "non-existent-defaulted")
Map.get(map, ":tricky_case") |> IO.inspect(label: "tricky_case")
Map.has_key?(map, "non-existent") |> IO.inspect(label: "has non-existent-key")
map[:tricky_case] |> IO.inspect(label: "tricky_case with access syntax")
map.tricky_case |> IO.inspect(label: "tricky_case with dot syntax")
map[:non_existent] |> IO.inspect(label: "non_existent with access syntax")
map.non_existent |> IO.inspect(label: "non_existent with dot syntax")
# Where all the keys of a map are atoms you can use this abbreviated syntax:
abbre_syntax = %{
  key_1: "value 1",
  key_2: "value 2"
}

normal_syntax = %{
  :key_1 => "value 1",
  :key_2 => "value 2"
}

abbre_syntax == normal_syntax

Structs Intro

defmodule Human do
  @enforce_keys [:name]
  defstruct name: "", age: 0, height: 0
end
# What's the output for each of these?
%Human{name: "Tim"}
%Human{name: "Tim", age: 20, height: 150}
%Human{age: 20, height: 150}
%Human{name: "Tim", species: "Human"}

Comparing structs to maps

Let's add some functionality to our Human module and contrast how this might look when using a struct vs a map...

defmodule HumanStruct do
  @enforce_keys [:name]
  defstruct name: "", age: 0, height: 0

  def grow_older(%HumanStruct{age: age} = human) do
    %HumanStruct{human | age: age + 1}
  end

  def grow_taller(%HumanStruct{height: height} = human) do
    %HumanStruct{human | height: height + 10}
  end
end

defmodule HumanMap do
  def grow_older(%{age: age} = human) do
    %{human | age: age + 1}
  end

  def grow_taller(%{height: height} = human) do
    %{human | height: height + 10}
  end
end
mike = %HumanStruct{name: "Mike", age: 25, height: 180}

mike
|> HumanStruct.grow_older()
|> HumanStruct.grow_taller()
|> IO.inspect(label: "Mike")

rachel = %{name: "Rachel", age: 48, height: 190}

rachel
|> HumanMap.grow_older()
|> HumanMap.grow_taller()
|> IO.inspect(label: "Rachel")

# What happens if we try and use a struct with a method expecting a map or vice versa?
mike |> HumanMap.grow_older()
rachel |> HumanStruct.grow_older()
DISCUSSION!
When do you think you should use maps and when should you use structs?

Keyword Lists

  • Keyword lists are just lists of 2 item tuples where the first element of each tuple is an atom
  • They are often used to pass function options in order to give a neat syntax for passing them
  • Keyword lists benefit from some syntactic sugar to make using them easier
  • The Keyword module has some helpful utils for working with Keyword lists
  • Possibly somewhat unobviously, they may contain elements with the same key more than once!
# A keyword list
[{:a, 1}, {:b, 2}]

# Shorthand for a keyword list
[a: 1, b: 2]

defmodule ListUtils do
  def query(list, kw_args \\ []) do
    filter_fns = Keyword.get_values(kw_args, :where)
    order_by = Keyword.get(kw_args, :order_by)

    filtered_list =
      Enum.reduce(filter_fns, list, fn filter_fn, list ->
        Enum.filter(list, filter_fn)
      end)

    Enum.sort(filtered_list, order_by)
  end
end

my_list = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]

res =
  ListUtils.query(
    my_list,
    where: fn x -> rem(x, 2) == 0 end,
    where: fn x -> rem(x, 3) == 0 end,
    order_by: fn x, y -> x > y end
  )

# This is just syntactic sugar for...
ListUtils.query(
  my_list,
  [
    {:where, fn x -> rem(x, 2) == 0 end},
    {:where, fn x -> rem(x, 3) == 0 end},
    {:order_by, fn x, y -> x > y end}
  ]
)

IO.inspect(res, label: "Result")
EXERCISE!

Write a function that takes 2 arguments - a starting number and a keyword list which can contain 2 keywords to_add, to_takeaway. It should return the starting number with the numbers to_add and to_takeaway added or taken away.

Example usage:

MyMath.do_math(5, to_takeaway: 5, to_takeaway: 2, to_add: 8)
# Expected result ---> 6

Pattern matching

Pattern matching can be helpful for:

  • extracting values from composite data structures like maps and tuples
  • conditional logic
{a, b} = {10, 20}
IO.inspect(a, label: "a")
IO.inspect(b, label: "b")

%{e: e} = %{e: 10, f: 15}
IO.inspect(e, label: "e")

[hd | tail] = [1, 2, 3]
IO.inspect(hd, label: "hd")
IO.inspect(tail, label: "tail")

[x, y, z] = [1, 2, 3]
IO.inspect(x, label: "x")
IO.inspect(y, label: "y")
IO.inspect(z, label: "z")
# When things don't match...
[a, b] = [1, 2, 3]

# You can use an underscore `_` or prefix a variable with an underscore e.g. `_tail` to help
# in situations where you only want to match parts
[a, b, _] = [1, 2, 3]
# Motivation for the pin operator
x = 10
[x, y, z] = [11, 12, 13]
IO.inspect(x, label: "x")
# Usage of the pin operator
x = 10
# --> Match Error!
[^x, y, z] = [11, 12, 13]
QUESTION!
Will the following pattern matches fail or succeed? What value will be set for x?
x = 5
[^x, ^x, x, x] = [5, 5, 6, 7]
[a | x] = [1, 2, 3]
x
{:ok, [x, _, _]} = {:error, [3, 4, 5]}
# Also known as the robot butt...
[_ | _] = []

Functions

Things to cover

  • Named functions
  • Anonymous functions, when to use, and how to pass named functions
  • Default args
  • 'when' qualifier
  • pattern matching with multiple function headers
# Named functions
defmodule Fn do
  def add(a, b) do
    a + b
  end

  def add_shorthand(a, b), do: a + b

  def add_defaulted(a \\ 0, b \\ 0), do: a + b
end

IO.inspect(Fn.add(10, 15))
IO.inspect(Fn.add_shorthand(10, 20))
IO.inspect(Fn.add_defaulted())
# Anonymous functions
add = fn a, b -> a + b end

# Call them with a .
add.(1, 5)

# You can pass anonymous functions as arguments to other functions
# e.g.1
Enum.map([1, 2, 3], fn x -> x * 2 end) |> IO.inspect(label: "Doubled")
# e.g.2
triple = fn x -> x * 3 end
Enum.map([1, 2, 3], triple) |> IO.inspect(label: "Tripled")
# How do we pass named functions as arguments?
defmodule Fn do
  def square(a), do: a * a
end

# You could use an anonymous function that calls your named function
Enum.map([1, 2, 3], fn x -> Fn.square(x) end)

# Neater to use function capture syntax
Enum.map([1, 2, 3], &Fn.square/1)
# Function capture syntax can also be used as a shorthand for defining anonymous functions
Enum.map([1, 2, 3], &(&1 * &1 * &1))
EXERCISE!
Can you write a function and_then which takes 2 functions as an argument and itself returns a function which combines them both?

Example usage:

combined_fn = Fn.and_then(fn a -> a + 5 end, fn a -> a * 3 end)
combined_fn.(7) # --> (7 + 5) * 3 --> 36
defmodule Fn do
  # ....
end

combined_fn = Fn.and_then(fn a -> a + 5 end, fn a -> a * 3 end)
combined_fn.(7)
BONUS 1!
Can you neaten up your code by using the pipe operator?
BONUS 2!
Can you adapt your code to combine any number of function calls by taking a list of functions as the method argument? TIP: Look at the reduce function
EXERCISE!
Can you use the capture syntax to define an anonymous function which takes 3 arguments and adds them together?

Multiple function headers

In Elixir we can define the same function multiple times, and put conditions about which implementation to use using 2 concepts:

  • Guards
  • Pattern matching
defmodule Guards do
  def double(a)

  def double(a) when is_number(a) do
    a * 2
  end

  # Strings are binaries in Elixir! Read more on the docs ;)
  def double(a) when is_binary(a) do
    a <> a
  end

  def double(a) when is_function(a) do
    fn x -> x |> a.() |> a.() end
  end
end

IO.inspect(Guards.double(5), label: "number")
IO.inspect(Guards.double("moo"), label: "string")
doubled_fn = Guards.double(fn x -> x * 2 end)
IO.inspect(doubled_fn.(2), label: "function")
defmodule PatternMatching do
  def get_favourite_pet(person, pets)

  def get_favourite_pet(_, []) do
    "No pets :("
  end

  def get_favourite_pet(%{name: name} = _person, [%{name: pet_name} | _]) do
    "#{pet_name} is #{name}'s favourite pet!"
  end
end

PatternMatching.get_favourite_pet(%{name: "Tim"}, [])
|> IO.inspect(label: "No pets")

PatternMatching.get_favourite_pet(%{name: "Joanna"}, [%{name: "Fluffy"}, %{name: "Cuddles"}])
|> IO.inspect(label: "Fluffy and Cuddles:")
EXERCISE!
Can you create a function struct_to_map that converts any struct to a map, even if it's nested?

Tips:

  • Map.from_struct() may come in handy
  • Map.new() may come in handy - it can create a map from a list of tuples
  • %_{} enables you to pattern match on any struct
  • Multiple function headers are your friend
defmodule MapHelpers do
  def struct_to_map() do
  end
end

defmodule Name do
  defstruct first_name: "", last_name: ""
end

defmodule Pet do
  defstruct name: Name
end

defmodule Human do
  defstruct age: 0, name: Name, pets: []
end
# Example usage
my_map =
  MapHelpers.struct_to_map(%Human{
    age: 35,
    name: %Name{first_name: "Tim", last_name: "G"},
    pets: [
      %Pet{name: %Name{first_name: "Fluffy", last_name: "Fluffball"}},
      %Pet{name: %Name{first_name: "Daisy", last_name: "Maisy"}}
    ]
  })

IO.inspect(my_map)

my_map == %{
  age: 35,
  name: %{first_name: "Tim", last_name: "G"},
  pets: [
    %{name: %{first_name: "Fluffy", last_name: "Fluffball"}},
    %{name: %{first_name: "Daisy", last_name: "Maisy"}}
  ]
}

Mix - the Elixir build tool

  • Mix is the build tool for Elixir, similar to npm, yarn, maven, sbt, pip, or whatever else you are used to!
  • It can...
    • Compile and run your project
    • Create new projects
    • Run your tests
    • Download your dependencies
    • Lots of other useful stuff...
  • Mix is great! But not very interesting, we'll just cover a few useful commands here
mix new my_new_project # Create a new project with the main module being MyNewProject. 
# Check the example folder for an example!
mix compile
MIX_ENV=test mix compile
iex -S mix # Start an iex session with all of your project code included!
mix deps.get
mix test
mix format

Some useful Phoenix and Ecto mix commands

Note for some of these to work you'll need to be in a Phoenix project or have the Phoenix pre-requisites installed

mix phx.server # Start the Phoenix server
mix phx.routes # Show the routes available in the application and their controllers
mix phx.new hello_world # Create a new Phoenix project
mix ecto.migrate # Run outstanding DB migrations
mix ecto.reset # Clears DB, re-runs migrations, and puts in some seed data

ExUnit - the Elixir testing library

You can find and run all of these examples in example/test/example_test.exs

Unfortunately they aren't setup to run in this Livebook.

A simple test case

defmodule ExampleTest do
  use ExUnit.Case # <--- Lets ExUnit setup the module for testing
  doctest Example # <--- says to run the doctests from the example module

  describe "hello" do # <---- describe can be used to group similar tests
    test "greets the world" do # <---- the actual test case 
      assert Example.hello() == :world
    end
  end
end

Using refute

test "doesn't sound like a police officer" do
  refute Example.hello() == :ello
end

Other useful helpers

  • assert_raise (built-in)
  • capture_io and capture_log (built-in)
  • assert_unordered_list_equality (custom)
defmodule Example2 do
  def greets_in_french do
    IO.inspect("greeting in french")
    :bonjour
  end

  def reverse_list(list), do: Enum.reverse(list)
end
defmodule Example2Test do
  use ExUnit.Case

  describe "greets_in_french/0" do
    test "it greets the user in French" do
      assert Example2.greets_in_french() == :bonjour
    end

    test "it does not greet the user in German" do
      refute Example2.greets_in_french() == :hallo
    end

    test "it raises if the function is called with the wrong number of arguments" do
      assert_raise FunctionClauseError, fn ->
        Example2.greets_in_french("some argument")
      end
    end

    test "it inspects as expected" do
      assert capture_io(fn -> Example2.greets_in_french() end)
    end
  end

  describe "reverse_list/1" do
    test "it returns a list containing the same elements as the input" do
      list = [1, 2, 3]
      result = Example.reverse(list)
      refute list == result
      assert_unordered_list_equality(list, result)
    end
  end
end

Doctests

Examples from docstrings on methods can be automatically tested!

@doc """
Hello world.

## Examples

    iex> Example.hello()
    :world

"""
def hello do
  :world
end

Test setup

  • setup_all runs once for the module
  • setup runs once before each test
  • Both can add to a state/context map, which is then available to each test
  • You can have multiple setup clauses
setup_all do
  IO.puts("Running setup_all step")
  [recipient: :world]
end

setup do
  IO.puts("Running setup step")

  on_exit(fn ->
    IO.puts("This is invoked once the test is done. Process: #{inspect(self())}")
  end)

  # Returns extra metadata to be merged into context.
  # Any of the following would also work:
  #
  #     {:ok, %{hello: "world"}}
  #     {:ok, [hello: "world"]}
  #     %{hello: "world"}
  #
  [hello: "world"]
end
defmodule Example3Test do
  use ExUnit.Case

  setup do
    :ok
  end

  describe "some_function_here/2" do
    setup do
      :ok
    end

    test "it behaves in a certain way" do
      ## assertions go here
    end

    test "it behaves in some other way" do
      ## assertions go here
    end
  end

  describe "some_other_function/3" do
    setup do
      :ok
    end

    test "it returns what we expect" do
      ## assertions go here
    end

    test "it raises when we expect it" do
      ## assertions go here
    end
  end

  test "some helper function does something" do
    ## assertions go here
  end
end

Using builders

Builders are a really big part of how we do Elixir testing at Multiverse.

A complex but highly used example of a builder is the Apprenticehip Builder, helping use build apprenticeship records quickly so we can test our code against various setups and situations. See: https://github.com/Multiverse-io/platform/blob/master/test/factories/apprenticeship_builder.ex

An example of how builders can be used can be found here: https://github.com/Multiverse-io/platform/blob/master/test/platform/flying_start_attendance/flying_start_attendance_filter_test.exs#L223

Project-wide test setup

test/test_helper.exs allows you to do some setup for all tests in your project.

Modules and docs

defmodule OuterModule do
  @moduledoc """
  I'm a module that does some stuff
  """
  defmodule InnerModule do
    @doc """
    Adds 2 numbers
    """
    def add(a, b), do: a + b
  end
end

defmodule OuterModule.AnotherInnerModule do
  def subtract(a, b), do: a - b
end

OuterModule.InnerModule.add(1, 2)
OuterModule.AnotherInnerModule.subtract(10, 2)

Control structures (case, cond, if, comprehensions, 'with')

db_results = {:ok, [%{name: "Tim", age: 21}]}

case db_results do
  {:ok, []} -> "No data!"
  {:ok, [_ | _] = data} -> data
  {:error, _} -> "Oh no an error!"
end
cond do
  2 + 2 == 5 -> "This will not be true"
  2 * 2 == 3 -> "Nor this"
  1 + 1 == 2 -> "But this will"
end
# Note there is no "else if"
if 2 + 2 == 5 do
  "This will not be true"
else
  "But this will"
end
unless 2 + 2 == 5 do
  "The laws of maths still hold"
end

Comprehensions

Comprehensions are useful as they allow you to do functions like Enum.map, Enum.filter in a neater, more concise syntax

# A simple example
for(n <- [1, 2, 3, 4], do: n * n)
|> IO.inspect(label: "Simple example")

# Pattern matching example - only keeps elements matching the pattern!
for({:a, num} <- [a: 1, b: 2, c: 3], do: num * 2)
|> IO.inspect(label: "Pattern matching example")

# Example with a filter
for(n <- [1, 2, 3, 4, 5], n <= 3, do: n * 2)
|> IO.inspect(label: "Example with filter")

# Example with multiple lists
for a <- [1, 2, 3],
    b <- [1, 2, 3] do
  a * b
end
|> IO.inspect(label: "Example with multiple lists")

# Example with a different enumerable as input 
for({animal, num} <- %{:monkeys => 10, :lions => 26}, do: {animal, num - 2})
|> IO.inspect(label: "Example with filter")

# Outputting to a data structure other than a list
for {key, val} <- [a: 1, b: 2, c: 3], into: %{}, do: {key, val * 2}
EXERCISE!
Use a comprehension to find the product (multiplication) of all even numbers from the first list, and all numbers less than 100 from the second list
# We'll just want to keep the even numbers
list_1 = [1, 2, 3, 4, 5]
# We'll just want to keep the numbers less than 100
list_2 = [27, 105, 109, 32, 2, 999]

# Final output should be a list containing: 
#  --> [2 * 27, 4 * 27, 2 * 32, 4 * 32, 2 * 2, 4 * 2]
#  --> [54, 108, 64, 128, 4, 8]

The With statement

Enables you to succintly execute a sequence of commands, and return immediately if any of the commands don't meet the given pattern match.

result = with 
  {:ok, db_results} <- get_db_data(),
  {:ok, parsed_results} <- parse_db_results(db_results) do
    IO.inspect(parsed_results)
  end
Motivation...
defmodule MaybeFailFns do
  def random_fail(), do: Enum.random([true, false])

  def read_file() do
    case random_fail() do
      true -> {:err, "file_does_not_exist"}
      false -> {:ok, "file contents"}
    end
  end

  def parse_file(_file_contents) do
    case random_fail() do
      true -> {:err, "cannot parse file"}
      false -> {:ok, "parsed file contents"}
    end
  end

  def write_parsed_output(_parsed_file) do
    case random_fail() do
      true -> {:err, "cannot write file"}
      false -> {:ok, "File written successfully!"}
    end
  end
end
# We want to read a file, parse the output, and then write the output to another file.
# Any of these steps may fail!!! We want to return the final result or the 1st error we get
import MaybeFailFns

# Approach 1 - things we've seen so far:
case read_file() do
  {:err, err} ->
    {:err, err}

  {:ok, file_contents} ->
    case parse_file(file_contents) do
      {:err, err} ->
        {:err, err}

      {:ok, parsed_file_contents} ->
        case write_parsed_output(parsed_file_contents) do
          {:err, err} -> {:err, err}
          {:ok, success_msg} -> {:ok, success_msg}
        end
    end
end
EXERCISE!
Use the with statement to neaten things up!
# Approach 2 - use the with to neaten things up!:
import MaybeFailFns

With statements also allow you to handle errors neatly rather than just returning them

We can use the else block of a with statement to pattern match on errors (in the case that not all the pattern matches in the with were successful)

result = with 
  {:ok, db_results} <- get_db_data(),
  {:ok, parsed_results} <- parse_db_results(db_results) do
    IO.inspect(parsed_results)
  else
    {:err, :db_connection_err} -> IO.puts("Couldn't connect to the DB, please try again!")
    {:err, :parse_error} -> IO.puts("We couldn't parse the results from the DB - please contact support!")
    _ -> IO.puts("We've encountered an unexpected error - please contact support")
  end
EXERCISE!
Can you add an else clause to the previous exercise to give more user friendly errors?

Alias, require, import, and use

Alias and Import let you use functions from other modules more succinctly

  • Aliasing lets you use a shorter reference for the module name
  • Import means you can use the function names directly
defmodule Some.Long.Nested.Module do
  def do_stuff(), do: "Stuff!"
end
Some.Long.Nested.Module.do_stuff()
# Or...
alias Some.Long.Nested.Module
Module.do_stuff()
# Or...
alias Some.Long.Nested.Module, as: M
M.do_stuff()

import Some.Long.Nested.Module
do_stuff()
DISCUSSION
When should we use import and when should we use alias?

Require lets you call macros from other modules

If you want to use macros from a module then you must require that module.

defmodule Mac do
  defmacro my_unless(clause, do: expression) do
    quote do
      if(!unquote(clause), do: unquote(expression))
    end
  end
end
require Mac

Mac.my_unless 1 == 2 do
  IO.puts("Woohoo!!")
end

if !(1 == 2), do: IO.puts("Woohoo!!")

Use runs the __using__ macro from the other module

When you 'use' another module it lets that module inject arbitrary code into your module. Usually you will rely on the documentation to let you know what it is doing. The Phoenix framework makes heavy use of this!

defmodule Mac do
  defmacro __using__(_opts) do
    IO.puts("We're using the Mac module!!")

    quote do
      def say_hello(), do: "hello"
    end
  end
end

defmodule UsingMac do
  use Mac

  def my_method, do: say_hello()
end

UsingMac.my_method()

Things to read up on outside of this training

  • Protocols
  • Macros
  • Elixir Processes and OTP