askesis

Elixir essentials: 50 cards on pattern matching, processes and OTP

Elixir fundamentals with short snippets: immutability, pattern matching, the pipe operator, Enum vs Stream, processes, GenServer, supervisors and mix.

ProgrammingIntermediate50 cardsFree

Practice now

Study without an account

0 of 50 seen0 graduated
Can you change a list or map in place in Elixir?

Scheduled with the default Askesis timings. Progress is saved in this browser.

All 50 cards

  1. 1
    Can you change a list or map in place in Elixir?
    No. Data is immutable: functions return new values, and x = x + 1 rebinds the name x to a new value without changing the old one
  2. 2
    What does = do in Elixir?
    It’s the match operator: it matches the right side against the pattern on the left, binding any variables, and raises MatchError if they don’t match
    elixir
    {:ok, body} = {:ok, "hello"}  # body is "hello"
    {:ok, body} = {:error, :enoent}  # ** (MatchError)
  3. 3
    What does the pin operator ^ do?
    Matches against a variable’s current value instead of rebinding the variable
    elixir
    x = 1
    x = 2   # rebinds x
    ^x = 3  # ** (MatchError), because x is 2
  4. 4
    What are atoms?
    Constants whose name is their value, such as :ok and :error; true, false and nil are atoms too
    Atoms are never garbage collected, so don’t create them from user input with String.to_atom/1.
  5. 5
    Lists vs tuples
    Lists are linked lists: prepending with [x | list] is fast, but length and index access are linear. Tuples are stored contiguously: index access is fast, but any update copies the whole tuple
  6. 6
    Keyword list vs map
    A keyword list ([name: "Ada", role: :admin]) is a list of {atom, value} tuples that keeps order and allows duplicate keys, typical for options; a map (%{name: "Ada"}) has unique keys of any type and fast lookup
  7. 7
    map.key vs map[:key]
    For an atom key, map.key raises KeyError when the key is missing, while map[:key] returns nil
  8. 8
    Update an existing key in a map
    elixir
    user = %{user | age: 37}
    This syntax raises KeyError if :age isn’t already in the map; Map.put/3 adds or replaces.
  9. 9
    Define a struct with a default value
    elixir
    defmodule User do
      defstruct [:name, age: 0]
    end
    
    %User{name: "Ada"}  # age defaults to 0
    Unknown keys are a compile-time error, and structs don’t support user[:name] access.
  10. 10
    "abc" vs ~c"abc"
    "abc" is a string, a UTF-8 encoded binary; ~c"abc" is a charlist, a list of code points used mainly with Erlang libraries
    Older code writes charlists with single quotes: 'abc'.
  11. 11
    What do String.length("héllo") and byte_size("héllo") return?
    5 and 6: String.length/1 counts graphemes, while byte_size/1 counts bytes, and é takes two bytes in UTF-8
  12. 12
    Which values are falsy in Elixir?
    Only false and nil; everything else, including 0, "" and [], is truthy
  13. 13
    Define a function with several clauses and a guard
    elixir
    defmodule Math do
      def fact(0), do: 1
      def fact(n) when n > 0, do: n * fact(n - 1)
    end
    Clauses are tried from top to bottom; the first one whose pattern and guard match runs.
  14. 14
    What can you call in a guard?
    Only a fixed set of expressions: comparisons, boolean operators, arithmetic, type checks like is_integer/1 and a few Kernel functions such as length/1
    Your own functions aren’t allowed; build reusable guards with defguard.
  15. 15
    What does [head | tail] = [1, 2, 3] bind?
    head to 1 and tail to [2, 3]
    Matching [head | tail] against [] raises a MatchError.
  16. 16
    Branch on an {:ok, _} or {:error, _} result
    elixir
    case File.read("config.json") do
      {:ok, body} -> parse(body)
      {:error, reason} -> {:error, reason}
    end
  17. 17
    What is with for?
    Chaining pattern matches: each <- clause must match to continue, and the first value that doesn’t match is returned (or handled in else)
    elixir
    with {:ok, body} <- File.read(path),
         {:ok, data} <- Jason.decode(body) do
      {:ok, data}
    end
  18. 18
    When do you use cond?
    For a chain of conditions that aren’t patterns: the first truthy one wins, so end with true -> as a catch-all
    If no condition is truthy, cond raises a CondClauseError.
  19. 19
    Give a function parameter a default value
    elixir
    def greet(name, greeting \\ "Hello"), do: "#{greeting}, #{name}"
  20. 20
    File.read/1 vs File.read!/1
    File.read/1 returns {:ok, contents} or {:error, reason}; the ! version returns the contents directly and raises on failure
    Many standard library functions come in this pair.
  21. 21
    What does |> do?
    Passes the result of the expression on its left as the first argument of the function call on its right
    elixir
    "hello world"
    |> String.split()
    |> Enum.map(&String.capitalize/1)
    |> Enum.join(" ")
    # "Hello World"
  22. 22
    What do &String.upcase/1 and &(&1 * 2) mean?
    Both use the capture operator: the first refers to a named function by name and arity, the second is shorthand for fn x -> x * 2 end
  23. 23
    Why is an anonymous function called with a dot, as in add.(1, 2)?
    The dot marks a call to a function stored in a variable, so it can’t be confused with a call to a named function add(1, 2)
  24. 24
    What does /2 mean in Enum.map/2?
    The arity: the number of arguments. Functions with the same name and different arities are separate functions
  25. 25
    Why can IO.inspect/2 sit in the middle of a pipeline?
    It prints its argument and returns it unchanged
    elixir
    orders
    |> Enum.filter(&paid?/1)
    |> IO.inspect(label: "paid")
    |> Enum.map(& &1.total)
  26. 26
    Enum vs Stream
    Enum functions are eager and build a full result at each step; Stream functions are lazy and compose into a single pass that runs only when something like an Enum function consumes it
  27. 27
    What does this return?
    elixir
    1..1_000_000
    |> Stream.map(&(&1 * 3))
    |> Stream.filter(&(rem(&1, 2) == 0))
    |> Enum.take(5)
    [6, 12, 18, 24, 30], after processing only the first ten numbers of the range
  28. 28
    Sum a list with Enum.reduce/3
    elixir
    Enum.reduce([1, 2, 3], 0, fn x, acc -> x + acc end)  # 6
    The function receives the element first and the accumulator second.
  29. 29
    What does for x <- 1..5, rem(x, 2) == 1, do: x * x return?
    [1, 9, 25]: a comprehension with a generator and a filter
  30. 30
    Count the lines containing ERROR in a large file without loading it all
    elixir
    File.stream!("app.log")
    |> Stream.filter(&String.contains?(&1, "ERROR"))
    |> Enum.count()
    File.stream!/1 reads the file lazily, line by line.
  31. 31
    What is an Elixir process?
    A lightweight process run by the BEAM virtual machine, with its own memory and mailbox; it isn’t an OS thread, and one system can run millions of them
  32. 32
    Spawn a process and send it a message
    elixir
    pid = spawn(fn ->
      receive do
        {:ping, from} -> send(from, :pong)
      end
    end)
    
    send(pid, {:ping, self()})
  33. 33
    Does send/2 wait for the receiver?
    No. It puts the message in the receiver’s mailbox and returns at once; receive is what blocks, until a matching message arrives
  34. 34
    Stop waiting in receive after five seconds
    elixir
    receive do
      {:result, value} -> value
    after
      5_000 -> :timeout
    end
  35. 35
    What happens to messages that match no receive clause?
    They stay in the mailbox, and receive keeps waiting for one that matches; unmatched messages pile up
  36. 36
    Link vs monitor
    A link is two-way: when one linked process crashes, the other gets an exit signal and crashes too unless it traps exits. A monitor is one-way: the watcher just receives a {:DOWN, ref, :process, pid, reason} message
    spawn_link/1 and Process.monitor/1 set them up.
  37. 37
    Run work concurrently and wait for its result
    elixir
    task = Task.async(fn -> fetch_report() end)
    report = Task.await(task)
    Task.await/2 exits the caller if the task takes longer than 5 seconds, unless you pass a timeout.
  38. 38
    What is a GenServer?
    An OTP behaviour for a process that keeps state and handles requests in a loop; you implement callbacks such as init/1, handle_call/3 and handle_cast/2
  39. 39
    GenServer.call vs GenServer.cast
    call is synchronous and waits for a reply, for up to 5 seconds by default; cast is asynchronous and returns :ok immediately
  40. 40
    A minimal counter GenServer
    elixir
    defmodule Counter do
      use GenServer
    
      def start_link(n), do: GenServer.start_link(__MODULE__, n, name: __MODULE__)
      def increment, do: GenServer.cast(__MODULE__, :inc)
      def value, do: GenServer.call(__MODULE__, :value)
    
      @impl true
      def init(n), do: {:ok, n}
    
      @impl true
      def handle_cast(:inc, n), do: {:noreply, n + 1}
    
      @impl true
      def handle_call(:value, _from, n), do: {:reply, n, n}
    end
  41. 41
    What is handle_info/2 for?
    Messages that arrive by plain send rather than through call or cast, such as timer messages and monitor :DOWN messages
  42. 42
    What does a supervisor do?
    Starts child processes and restarts them, following a strategy, when they crash
  43. 43
    Supervisor strategies :one_for_one, :one_for_all and :rest_for_one
    Restart only the crashed child; restart every child; restart the crashed child and the children started after it
  44. 44
    Start children under a supervisor
    elixir
    children = [
      {Counter, 0},
      {Task.Supervisor, name: MyApp.TaskSupervisor}
    ]
    
    Supervisor.start_link(children, strategy: :one_for_one)
    {Counter, 0} works because use GenServer defines child_spec/1, which calls Counter.start_link(0).
  45. 45
    Child restart values :permanent, :transient and :temporary
    :permanent children are always restarted (the default); :transient ones only after an abnormal exit; :temporary ones never
  46. 46
    What does “let it crash” mean?
    Don’t write defensive code for every unexpected error: let the process crash and have its supervisor restart it in a known good state
  47. 47
    Create a new project with a supervision tree
    bash
    mix new my_app --sup
    --sup generates an application module that starts a supervisor.
  48. 48
    Fetch dependencies, run the tests and open a shell with the project loaded
    bash
    mix deps.get
    mix test
    iex -S mix
  49. 49
    Where do you declare a project’s dependencies?
    In the deps/0 function of mix.exs
    elixir
    defp deps do
      [{:jason, "~> 1.4"}]
    end
  50. 50
    What does the requirement "~> 1.4" allow?
    Any version from 1.4.0 up to, but not including, 2.0.0
    "~> 1.4.2" is stricter: from 1.4.2 up to, but not including, 1.5.0.

Frequently asked questions

Do I need to know Erlang?

No. Every card uses Elixir syntax. Erlang and the BEAM virtual machine only come up where they explain how processes behave.

Which Elixir version do the cards target?

Current stable Elixir. Details that changed between versions, such as how charlists are written, are noted on the card.

Does it cover Phoenix?

No. It covers the language, processes and OTP that Phoenix is built on, which makes Phoenix code much easier to follow.

Intermediate

JavaScript

Master the JavaScript every developer uses daily: scope, closures, equality, array methods, promises, async/await and the event loop.

50 cards
Intermediate

Advanced JS

Fifty advanced JavaScript cards: event-loop ordering puzzles, generators and async iteration, this binding, Proxy and Reflect, modules, AbortController and coercion edge cases.

50 cards
Intermediate

TypeScript

Learn the TypeScript you use every day: narrowing, generics, keyof, mapped and conditional types, utility types, satisfies and strict mode, with code on every card.

50 cards
Beginner

Python

Learn idiomatic Python fast: comprehensions, f-strings, args and kwargs, generators, decorators, context managers and the standard library.

50 cards
Beginner

Git & terminal

The Git commands and shell skills developers use every day: branching, rebasing, undoing mistakes, pipes, redirects, permissions and processes.

50 cards
Beginner

Java

Core Java with short snippets: equals vs ==, records, sealed types, collections, generics, exceptions, streams, lambdas and JVM basics.

50 cards
Beginner

C#

C# and .NET fundamentals with short snippets: value vs reference types, properties, records, nullable reference types, LINQ, async/await and DI.

50 cards
Beginner

Kotlin

Kotlin fundamentals with short snippets: null safety, val vs var, data and sealed classes, extension functions, scope functions and coroutines.

50 cards
Beginner

Swift

Swift fundamentals with short snippets: optionals, structs vs classes, protocols, enums with associated values, closures, ARC, async/await and SwiftUI.

50 cards
Beginner

Flutter

Flutter and Dart basics: null safety, futures and streams, stateless vs stateful widgets, setState, hot reload, keys, layout and state management.

50 cards
Beginner

PHP

Modern PHP 8 with short snippets: strict types, arrays, match, nullsafe and named arguments, enums, readonly, Composer, PDO and password_hash.

50 cards
Beginner

Ruby

Ruby fundamentals with short snippets: blocks, procs and lambdas, symbols, Enumerable, mixins, truthiness, method_missing, gems and Bundler.

50 cards
Beginner

Go

Go (Golang) fundamentals with short, idiomatic snippets: slices vs arrays, maps, defer, error wrapping, interfaces, goroutines, channels, select and context.

50 cards
Intermediate

Rust

Learn the Rust that the compiler keeps asking about: ownership, borrowing, lifetimes, Option and Result, traits, closures, smart pointers and async.

50 cards
Intermediate

C++

Modern C++ in short snippets: references vs pointers, RAII, smart pointers, move semantics, STL containers and algorithms, const, templates and UB.

50 cards
Intermediate

Haskell

Haskell fundamentals with short code: types and type classes, pattern matching, laziness, Maybe and Either, folds, Functor, Applicative and Monad.

50 cards
Intermediate

x86-64 assembly

x86-64 assembly in Intel syntax: registers and sub-registers, flags, mov and lea, jumps, the stack frame, the System V calling convention and Linux syscalls.

50 cards
Beginner

Regex

Learn regular expressions for good: anchors, classes, greedy vs lazy quantifiers, groups, lookarounds, flags, replacements and the backtracking trap.

50 cards