Skip to content

Elixir 치트시트

Elixir는 확장 가능하고 유지보수 가능한 애플리케이션을 구축하기 위한 동적 함수형 언어입니다.

01

기본

Hello World

IO.puts는 줄바꿈과 함께 출력합니다

elixir
# IEx interactive shell
# run: iex

# simple values
iex> 1 + 2
3
iex> "hello" <> " world"
"hello world"

# atoms (constants where name is value)
iex> :ok
:ok
iex> :error
:error

# tuples
iex> {:ok, 42}
{:ok, 42}

# lists
iex> [1, 2, 3]
[1, 2, 3]

주석

단일 줄 주석만 가능합니다

elixir
# hello.exs
IO.puts("Hello, World!")

# run: elixir hello.exs
# or in iex: iex hello.exs

# string interpolation with #{}:
name = "Elixir"
IO.puts("Hello, #{name}!")
# Hello, Elixir!

# IO.inspect shows any value (useful for debugging):
IO.inspect([1, 2, 3], label: "nums")

iex 대화형 셸

iex는 Elixir의 대화형 셸입니다

elixir
# single line comment only
# Elixir has no multi-line comments

# script file (.exs) - executed directly
# elixir script.exs

# compiled module file (.ex) - compiled then run
# elixirc file.ex

# in IEx, reload a single module:
iex> r MyModule

# recompile the whole project:
iex> recompile()

불변 데이터

데이터는 불변이며, 연산은 새 값을 반환합니다

elixir
# in IEx:
iex> h Enum.map        # docs for Enum.map/2
iex> h Enum            # module docs
iex> i "hello"         # type info about a value
iex> b GenServer       # list behaviour callbacks
iex> s Enum.map        # function spec
iex> t Enum.t          # print type definitions

# exit IEx:
iex> System.halt(0)
# or press Ctrl+C twice

Atoms

Atom은 상수이며, 콜론으로 시작합니다

elixir
# Mix project layout:
my_app/
  lib/           # source code (.ex)
    my_app.ex
    my_app/greeter.ex
  test/          # ExUnit tests (.exs)
    my_app_test.exs
  mix.exs        # project config & dependencies
  config/        # config files
    config.exs

# module names map to file paths:
# MyApp.Greeter -> lib/my_app/greeter.ex
defmodule MyApp.Greeter do
  def hello(name), do: "Hi, #{name}"
end
02

패턴 매칭

매치 연산자

=는 할당이 아닌 매치입니다

elixir
# atoms are constants where the name IS the value
:ok
:error
:success
:true   # boolean true is an atom
:false  # boolean false is an atom
nil     # nil is also an atom

# create from a string:
String.to_atom("hello")   # :hello

# atoms compared by name (alphabetical):
:apple < :banana   # true

# existing? check:
Atom.to_string(:ok)   # "ok"

매치 무시

_는 원하지 않는 값을 무시합니다

elixir
# integers (arbitrary precision)
42
-7
0x1F    # hex = 31
0o17    # octal = 15
0b1010  # binary = 10

# floats
3.14
-0.5
1.0e3   # 1000.0

# division always returns a float:
7 / 2       # 3.5
div(7, 2)   # 3  (integer division)
rem(7, 3)   # 1  (remainder)

# rounding:
round(3.6)  # 4
trunc(3.9)  # 3

함수에서 패턴 매칭

여러 절이 순서대로 매치됩니다

elixir
# double-quoted strings are UTF-8 binaries
"hello"
"Elixir"           # UTF-8 supported
"#{1 + 1} cats"    # "2 cats"

# single-quoted are charlists (list of codepoints)
'hello' == [104, 101, 108, 108, 111]   # true

# heredoc for multiline strings:
"""
line 1
line 2
"""

is_binary("hi")   # true
is_list('hi')     # true

case 표현식

case는 패턴 매칭을 수행합니다

elixir
true
false
nil

# only false and nil are falsy:
if nil, do: "yes", else: "no"   # "no"
if 0, do: "yes", else: "no"     # "yes" (0 is truthy!)
if [], do: "yes", else: "no"    # "yes" (empty list is truthy)

# strict boolean ops (require booleans):
true and false   # false
true or false    # true
not true         # false

# truthy ops (any value, return the value):
nil && 1     # nil
1 && 2       # 2
nil || "x"   # "x"

가드

when은 가드 조건을 추가합니다

elixir
# list (linked list)
[1, 2, 3]
[head | tail] = [1, 2, 3]   # head=1, tail=[2,3]
[:a, "b", 3]                # heterogeneous ok

# tuple (fixed-size, contiguous memory)
{:ok, 42}
{1, 2, 3}
tuple_size({:a, :b, :c})    # 3

# keyword list (list of 2-tuples with atom keys)
[name: "Alice", age: 30]

# map (any keys)
%{"name" => "Bob", :age => 25}
03

불변 데이터

불변 리스트

모든 연산은 새 데이터를 반환합니다

elixir
1 + 2      # 3
5 - 3      # 2
2 * 4      # 8
10 / 2     # 5.0  (always float)
div(7, 2)  # 3    (integer division)
rem(7, 3)  # 1    (remainder, sign of dividend)
abs(-5)    # 5
round(3.6) # 4
trunc(3.9) # 3

# power (Elixir 1.12+):
Integer.pow(2, 10)  # 1024
:math.pow(2, 10)    # 1024.0 (float)

불변 Map

업데이트는 새 Map을 반환합니다

elixir
1 == 1.0     # true   (value equality, ignores type)
1 === 1.0    # false  (strict, also checks type)
1 != 2       # true
1 !== 1.0    # true
2 > 1        # true
2 >= 2       # true
1 < 2        # true
1 <= 2       # true

# total ordering across types:
# number < atom < ref < fun < port < pid < tuple < map < list < bitstring
:atom > 1    # true
[1] > %{}    # true

참조 동등성

참조가 아닌 값을 비교합니다

elixir
# strict (require boolean operands):
true and false   # false
true or false    # true
not true         # false

# truthy (any operand, return the operand):
nil && 1          # nil
1 && 2            # 2
nil || "default"  # "default"
false || 5        # 5
!nil              # true
!1                # false

# short-circuit evaluation:
false and raise("never")   # false (right side skipped)
true or raise("never")     # true  (right side skipped)

String Concatenation

<> concatenates binaries (strings); ++ concatenates lists (including charlists). -- removes the first occurrence of each element on the right. Prefer interpolation over repeated <> for readability.

elixir
"hello" <> " " <> "world"   # "hello world"

# interpolation (preferred):
name = "Alice"
"Hi, #{name}!"               # "Hi, Alice!"
"Sum: #{1 + 2}"              # "Sum: 3"

# charlist concatenation:
'abc' ++ 'de'   # 'abcde'

# list concatenation and subtraction:
[1, 2] ++ [3, 4]   # [1, 2, 3, 4]
[1, 2, 3] -- [2]   # [1, 3]  (removes first match)

Match Operator

= matches the right side against the left pattern, binding variables. If values don't match, raises MatchError. Variables can be rebound (point to new values), but the underlying data is immutable.

elixir
# = is pattern matching, not assignment
x = 1
1 = x          # ok (matches, since x is 1)
2 = x          # raises MatchError

# destructuring:
{a, b} = {1, 2}
a   # 1
b   # 2

[left | rest] = [1, 2, 3]   # left=1, rest=[2,3]

# variables can be rebound (data itself is immutable):
x = 1
x = 2          # rebinds x to 2

Membership & Others

in/2 checks membership in lists, ranges, maps (keys), and binaries (substring). | prepends to a list and is also used in pattern matching for head/tail. |> is the pipe operator (covered in its own section).

elixir
# in/2 checks membership in an enumerable:
2 in [1, 2, 3]        # true
:b in [:a, :b, :c]    # true
"x" in "text"         # true (substring in binary)

# range membership:
5 in 1..10            # true

# | prepends to a list:
[0 | [1, 2, 3]]       # [0, 1, 2, 3]

# also used to split head/tail:
[head | tail] = [1, 2, 3]

# |> is the pipe operator (see Pipe section):
"hi" |> String.upcase()   # "HI"
04

모듈

모듈 정의

defmodule은 모듈을 정의합니다

elixir
# = matches right against left
x = 42
{a, b, c} = {1, 2, 3}
[a, b, c] = [:x, :y, :z]

# reusing a variable in one pattern forces equality:
{n, n} = {1, 1}    # ok, n = 1
{n, n} = {1, 2}    # MatchError (1 != 2)

# ignore values with _:
{_, b, _} = {1, 2, 3}   # b = 2

# bind multiple at once:
{_, {x, y}} = {:ok, {3, 4}}   # x=3, y=4

모듈 속성

@는 모듈 속성을 정의합니다

elixir
x = 1

# ^ pins a variable: match its current value, don't rebind
^x = 1          # ok (x already 1)
^x = 2          # MatchError

case {1, 2} do
  {^x, second} -> "matched, second=#{second}"
  _ -> "no match"
end
# "matched, second=2"

# in function heads:
def same_as_x?(^x), do: true
def same_as_x?(_), do: false

private 함수

defp는 private 함수를 정의합니다

elixir
{:ok, value} = {:ok, 42}
value   # 42

{:error, reason} = {:error, :not_found}
reason  # :not_found

# size must match:
{a, b} = {1, 2, 3}   # MatchError (size 2 vs 3)

# nested matching:
{:user, {name, age}} = {:user, {"Alice", 30}}
name   # "Alice"

# tagged tuples for results:
case File.read("missing.txt") do
  {:ok, content}      -> content
  {:error, :enoent}   -> "file not found"
end

모듈 중첩

모듈은 중첩될 수 있습니다

elixir
# head and tail:
[head | tail] = [1, 2, 3]
head   # 1
tail   # [2, 3]

# fixed first elements + rest:
[a, b | rest] = [1, 2, 3, 4]
a      # 1
b      # 2
rest   # [3, 4]

# empty list matches only []:
[] = []    # ok
[] = [1]   # MatchError

# classic recursion pattern:
def sum([]), do: 0
def sum([head | tail]), do: head + sum(tail)

Matching Maps

Map matching only checks the keys you name—a subset match succeeds even if extra keys exist. Use ^key => value to match a dynamic key. Structs match by name and fields.

elixir
# match on specific keys (subset is fine):
%{name: name} = %{name: "Alice", age: 30}
name   # "Alice"

# match a specific value:
%{status: :active} = %{status: :active, id: 1}   # ok
%{status: :closed} = %{status: :active}          # MatchError

# variable as a key requires pinning:
key = :name
%{^key => value} = %{name: "Bob"}
value   # "Bob"

# struct matching (also a map):
%User{name: n} = %User{name: "Carol", age: 40}

Matching in Functions

Function clauses are tried in order; the first matching head wins. Pattern matching + guards replace if/else for dispatch. Include a catch-all clause (or FunctionClauseError fires on no match).

elixir
defmodule Geometry do
  # clauses tried top to bottom; first match wins:
  def area({:rectangle, w, h}), do: w * h
  def area({:circle, r}), do: 3.14 * r * r
  def area({:square, s}), do: s * s
  def area(_), do: {:error, :unknown_shape}
end

Geometry.area({:rectangle, 3, 4})   # 12
Geometry.area({:circle, 2})         # 12.56
Geometry.area({:triangle, 3, 4})    # {:error, :unknown_shape}

# guards add conditions:
def classify(n) when n < 0, do: :negative
def classify(0), do: :zero
def classify(n) when n > 0, do: :positive
05

함수

이름 있는 함수

def는 이름 있는 함수를 정의합니다

elixir
# data is never mutated; operations return new data
list = [1, 2, 3]
List.replace_at(list, 0, 99)   # [99, 2, 3]
list                           # [1, 2, 3] (unchanged)

map = %{a: 1}
Map.put(map, :b, 2)            # %{a: 1, b: 2}
map                            # %{a: 1} (unchanged)

# strings too:
s = "hello"
String.upcase(s)   # "HELLO"
s                  # "hello"

기본 인자

\\는 기본값을 설정합니다

elixir
original = [1, 2, 3]

# prepend (fast, O(1)):
[0 | original]         # [0, 1, 2, 3]
original               # [1, 2, 3]

# append (slow, O(n)):
original ++ [4]        # [1, 2, 3, 4]
original               # [1, 2, 3]

# concatenation:
[1, 2] ++ [3, 4]       # [1, 2, 3, 4]

다중 절 함수

인자를 순서대로 매치합니다

elixir
m = %{name: "Alice", age: 30}

# Map.put (add or update):
Map.put(m, :age, 31)        # %{name: "Alice", age: 31}

# update syntax (key MUST exist):
%{m | age: 31}              # %{name: "Alice", age: 31}
# %{m | height: 170}        # KeyError (height not present)

# add only if missing:
Map.put_new(m, :role, "admin")   # adds :role
Map.put_new(m, :age, 99)         # unchanged (age exists)

# delete:
Map.delete(m, :age)         # %{name: "Alice"}

파이프라인 함수

|> 파이프 연산자

elixir
# variables bind to values; rebinding is allowed but
# the bound VALUE never changes
x = [1, 2, 3]
y = x              # y points to the same list
x = [4, 5]         # x rebound, y still [1, 2, 3]
y                  # [1, 2, 3]

# there is NO in-place mutation API:
# list[0] = 99    -- not valid Elixir

# to "update", rebind the variable:
x = [99 | tl(x)]   # x now points to a new list

Sharing & Efficiency

Persistent data structures share unmodified parts, so 'copies' are cheap (O(1) for list prepend, ~O(log n) for map updates). Immutability makes sharing safe across processes without locks or copying.

elixir
# immutability enables safe structural sharing:
base = [1, 2, 3, 4, 5]
shared = [0 | base]    # [0, 1, 2, 3, 4, 5]
# `shared` reuses `base`'s nodes—no copy of the tail

# maps also share internally; updates are ~O(log n):
m = %{a: 1, b: 2, c: 3}
m2 = Map.put(m, :d, 4)   # shares most of m

# because data is immutable, sharing is always safe—
# no defensive copies needed across processes.
pids = Enum.map(1..100, fn _ -> spawn(fn -> m end) end)
06

익명 함수

익명 함수

fn..end는 익명 함수를 정의합니다

elixir
[head | tail] = [1, 2, 3]
head          # 1
tail          # [2, 3]

hd([1, 2, 3]) # 1
tl([1, 2, 3]) # [2, 3]

# empty list has no head:
# hd([])   # raises ArgumentError

# prepend a first element:
[0 | [1, 2, 3]]   # [0, 1, 2, 3]

# length is O(n):
length([1, 2, 3])  # 3

이름 있는 함수 캡처

&Module.function/arity는 함수를 캡처합니다

elixir
[1, 2] ++ [3, 4]    # [1, 2, 3, 4]
[] ++ [1]           # [1]

# subtraction removes first occurrence of each right element:
[1, 2, 2, 3, 2] -- [2]   # [1, 2, 3, 2]  (only first 2)
[1, 2, 3] -- [4]         # [1, 2, 3]      (no-op)

# flatten nested lists:
List.flatten([1, [2, [3, 4]], 5])   # [1, 2, 3, 4, 5]

# fold (left):
List.foldl([1, 2, 3], 0, fn x, acc -> x + acc end)  # 6

클로저

익명 함수는 외부 변수를 캡처합니다

elixir
# basic:
for n <- [1, 2, 3], do: n * 2        # [2, 4, 6]

# with filter:
for n <- 1..10, rem(n, 2) == 0, do: n   # [2, 4, 6, 8, 10]

# multiple generators:
for x <- [:a, :b], y <- [1, 2], do: {x, y}
# [a: 1, a: 2, b: 1, b: 2]

# into: change the result container:
for n <- [1, 2, 3], into: %{}, do: {n, n * n}
# %{1 => 1, 2 => 4, 3 => 9}

# build a string:
for c <- ?a..?c, into: "", do: <<c>>   # "abc"

다중 절 익명 함수

익명 함수도 여러 절을 지원합니다

elixir
List.first([1, 2, 3])         # 1
List.last([1, 2, 3])          # 3
List.delete([1, 2, 3], 2)     # [1, 3]
List.delete_at([1, 2, 3], 1)  # [1, 3]
List.insert_at([1, 2, 3], 1, 9)   # [1, 9, 2, 3]
List.replace_at([1, 2, 3], 1, 9)  # [1, 9, 3]

# wrap (ensure list):
List.wrap(nil)    # []
List.wrap(1)      # [1]
List.wrap([1, 2]) # [1, 2]

# duplicate:
List.duplicate(:x, 3)   # [:x, :x, :x]

Enumerating Lists

Enum works on any enumerable (lists, maps, ranges). map/filter/reduce are the workhorses. chunk_every/2 splits into groups of size n; zip/2 pairs elements from two lists.

elixir
Enum.map([1, 2, 3], fn x -> x * 10 end)        # [10, 20, 30]
Enum.filter([1, 2, 3, 4], fn x -> x > 2 end)    # [3, 4]
Enum.reduce([1, 2, 3], 0, &+/2)                 # 6
Enum.find([1, 2, 3], fn x -> x > 1 end)         # 2

# membership:
1 in [1, 2, 3]                # true
Enum.member?([1, 2, 3], 2)    # true

# chunking:
Enum.chunk_every([1, 2, 3, 4], 2)   # [[1, 2], [3, 4]]

# zip:
Enum.zip([:a, :b], [1, 2])   # [{:a, 1}, {:b, 2}]

Flattening and Folding

List.flatten/1 removes nesting. Enum.reduce/3 is the idiomatic left fold. List.foldr/3 folds from the right. Folding with prepend builds a reversed list efficiently in O(n).

elixir
# flatten:
List.flatten([1, [2, 3], [4, [5]]])   # [1, 2, 3, 4, 5]

# flatten with a tail:
List.flatten([1, [2]], [3, 4])        # [1, 2, 3, 4]

# fold (Enum.reduce is a left fold):
Enum.reduce([1, 2, 3], 0, fn x, acc -> acc + x end)   # 6

# right fold:
List.foldr([1, 2, 3], [], fn x, acc -> [x | acc] end) # [1, 2, 3]

# building a reversed list with reduce + prepend:
Enum.reduce([1, 2, 3], [], fn x, acc -> [x | acc] end) # [3, 2, 1]
07

Enum 모듈

map과 each

map은 새 리스트를 반환, each는 :ok를 반환합니다

elixir
{:ok, 42}
{1, 2, 3}
{}
{:point, 3, 4}

# from a list:
List.to_tuple([:a, :b, :c])   # {:a, :b, :c}

# duplicate elements:
Tuple.duplicate(:x, 3)        # {:x, :x, :x}

# size (O(1)):
tuple_size({:a, :b, :c})      # 3

filter와 reject

filter는 유지, reject는 제외합니다

elixir
t = {:ok, "hello", 42}

elem(t, 0)    # :ok
elem(t, 1)    # "hello"
elem(t, 2)    # 42

# tuples are 0-indexed; out of range raises:
# elem(t, 5)   # ArgumentError

# put_elem returns a NEW tuple:
put_elem(t, 1, "world")   # {:ok, "world", 42}
t                          # unchanged: {:ok, "hello", 42}

# pattern matching is preferred over index access:
{:ok, msg, _} = t
msg   # "hello"

reduce

reduce는 리스트를 집계합니다

elixir
# idiomatic return values:
def divide(_a, 0), do: {:error, :divide_by_zero}
def divide(a, b), do: {:ok, div(a, b)}

# handle with case:
case divide(10, 2) do
  {:ok, result}     -> "result: #{result}"
  {:error, reason}  -> "error: #{reason}"
end
# "result: 5"

# with chains matches across steps:
with {:ok, a} <- maybe_a(),
     {:ok, b} <- maybe_b() do
  a + b
end

찾기

find는 요소를 반환, find_value는 함수 결과를 반환합니다

elixir
t = {1, 2, 3}

# put_elem returns a new tuple:
put_elem(t, 0, 99)        # {99, 2, 3}

# tuples are immutable:
t                         # {1, 2, 3}

# append/prepend/insert create a NEW tuple (O(n)):
Tuple.append(t, 4)        # {1, 2, 3, 4}
Tuple.insert_at(t, 1, 99) # {1, 99, 2, 3}

# delete:
Tuple.delete_at(t, 1)     # {1, 3}

정렬

sort_by는 지정된 키로 정렬합니다

elixir
t = {:a, :b, :c}

Tuple.append(t, :d)         # {:a, :b, :c, :d}
Tuple.delete_at(t, 0)       # {:b, :c}
Tuple.duplicate(:x, 3)      # {:x, :x, :x}
Tuple.insert_at(t, 1, :z)   # {:a, :z, :b, :c}

# conversions:
Tuple.to_list(t)            # [:a, :b, :c]
List.to_tuple([1, 2, 3])    # {1, 2, 3}

# size:
tuple_size(t)               # 3
08

Stream 모듈

지연 시퀀스

Stream은 지연 평가됩니다

elixir
# literal syntax (list of 2-tuples with atom keys):
[name: "Alice", age: 30]
# equivalent to:
[{:name, "Alice"}, {:age, 30}]

# duplicate keys allowed:
[a: 1, a: 2]   # [a: 1, a: 2]

# empty:
[]

# a keyword list is just a list:
is_list([a: 1])      # true
length([a: 1, b: 2]) # 2

무한 Stream

Stream.iterate는 무한 Stream을 생성합니다

elixir
kw = [name: "Alice", age: 30, role: :admin]

# access first value for a key:
kw[:name]    # "Alice"
kw[:age]     # 30
kw[:missing] # nil

# Keyword.get with a default:
Keyword.get(kw, :missing, "default")  # "default"

# get ALL values for a key (when duplicates exist):
Keyword.get_values([a: 1, a: 2], :a)  # [1, 2]

# strict fetch (raises if missing):
Keyword.fetch!(kw, :name)   # "Alice"

순환 Stream

Stream.cycle는 무한히 반복합니다

elixir
kw = [a: 1, b: 2, c: 3]

Keyword.keys(kw)          # [:a, :b, :c]
Keyword.values(kw)        # [1, 2, 3]
Keyword.has_key?(kw, :a)  # true

Keyword.put(kw, :d, 4)    # [a: 1, b: 2, c: 3, d: 4]
Keyword.delete(kw, :b)    # [a: 1, c: 3]

# merge (right side wins on conflicts):
Keyword.merge([a: 1], [a: 99, b: 2])  # [a: 99, b: 2]

# take/drop:
Keyword.take(kw, [:a, :c])   # [a: 1, c: 3]

리소스 Stream

Stream.resource는 리소스를 관리합니다

elixir
# Keyword list: ordered, atom keys only, duplicates, O(n)
[name: "Alice", age: 30]

# Map: unordered, any keys, no duplicates, ~O(log n)
%{name: "Alice", age: 30}

# use keyword lists for function options (idiomatic):
String.split("a,b,c", ",", trim: true)

# use keyword lists for small ordered config:
Application.get_env(:my_app, :key, [])

# use maps for:
# - key/value data with mixed-type or string keys
# - frequent lookups

Common Use Cases

Keyword lists shine as the last argument of functions for options (e.g., trim: true). The do/end block syntax is actually a keyword list in disguise: if(true, do: x, else: y). Config files use them heavily.

elixir
# function options (most common):
def greet(name, opts \\ []) do
  prefix = Keyword.get(opts, :prefix, "Hello")
  "#{prefix}, #{name}!"
end
greet("Alice", prefix: "Hi")   # "Hi, Alice!"

# config files:
config :my_app, MyRepo,
  pool_size: 10,
  timeout: 5000

# do/end block sugar is a keyword list:
if true, do: :yes, else: :no
# is shorthand for:
if(true, do: :yes, else: :no)
09

리스트 연산

리스트 기본

리스트는 연결 리스트 구조입니다

elixir
# atom keys (shorthand):
%{name: "Alice", age: 30}

# any keys (=>):
%{"name" => "Bob", 1 => :one, :ok => true}

# mixed keys:
%{:atom => 1, "string" => 2}

# from a keyword list:
Enum.into([a: 1, b: 2], %{})   # %{a: 1, b: 2}

# empty:
%{}

# size (O(1)):
map_size(%{a: 1, b: 2})   # 2

연결

++는 연결, --는 제거합니다

elixir
m = %{name: "Alice", age: 30, role: :admin}

# bracket access (nil if missing):
m[:name]    # "Alice"
m[:missing] # nil

# dot access (atom keys only, raises if missing):
m.name      # "Alice"
# m.missing  # KeyError

# strict fetch:
Map.fetch!(m, :age)    # 30
Map.fetch(m, :missing) # :error

# with a default:
Map.get(m, :missing, "default")  # "default"

리스트 컴프리헨션

for 컴프리헨션은 필터링과 변환을 지원합니다

elixir
m = %{name: "Alice", age: 30}

# update existing key (raises if missing):
%{m | age: 31}              # %{name: "Alice", age: 31}

# add or update:
Map.put(m, :age, 31)        # %{name: "Alice", age: 31}
Map.put(m, :role, :admin)   # adds :role

# add only if missing:
Map.put_new(m, :role, :admin)

# delete:
Map.delete(m, :age)         # %{name: "Alice"}

# merge (right side wins on conflicts):
Map.merge(%{a: 1}, %{a: 99, b: 2})   # %{a: 99, b: 2}

List 모듈

List 모듈은 리스트 연산을 제공합니다

elixir
m = %{a: 1, b: 2, c: 3}

Map.keys(m)           # [:a, :b, :c] (order not guaranteed)
Map.values(m)         # [1, 2, 3]
Map.has_key?(m, :a)   # true
Map.take(m, [:a, :c]) # %{a: 1, c: 3}
Map.drop(m, [:b])     # %{a: 1, c: 3}

# transform:
Map.new([{:a, 1}, {:b: 2}])   # %{a: 1, b: 2}
Map.to_list(m)                # [a: 1, b: 2, c: 3]

# nested update:
users = %{alice: %{age: 30}}
put_in(users, [:alice, :age], 31)   # %{alice: %{age: 31}}

Pattern Matching Maps

Map patterns match on a subset of keys—extra keys are fine. To match a variable key, pin it with ^. This makes maps excellent for destructuring API responses and configuration.

elixir
# subset matching (extra keys ignored):
%{name: name} = %{name: "Alice", age: 30}
name   # "Alice"

# match a specific value:
%{status: :active} = %{status: :active, id: 1}   # ok

# variable key (must be pinned):
key = :name
%{^key => value} = %{name: "Bob"}
value   # "Bob"

# in case clauses:
case response do
  %{status_code: 200, body: body} -> body
  %{status_code: code}            -> {:error, code}
end

Structs

Structs are tagged maps with a fixed set of keys and defaults, defined with defstruct. They carry their module name (__struct__) and enforce their keys at compile time—use them for domain entities.

elixir
defmodule User do
  defstruct name: "anon", age: 0, role: :user
end

# create:
%User{name: "Alice", age: 30}
# %User{age: 30, name: "Alice", role: :user}

# access:
u = %User{name: "Bob"}
u.name          # "Bob"
u.__struct__    # User

# update (existing fields only):
%{u | age: 25}  # %User{age: 25, name: "Bob", role: :user}

# unknown key raises at compile time:
# %User{height: 180}   # KeyError / compile error

# pattern match:
%User{name: n} = u
n   # "Bob"
10

Map 연산

Map 생성

%{}는 Map을 생성합니다

elixir
Enum.map([1, 2, 3], fn x -> x * 2 end)        # [2, 4, 6]
Enum.map([1, 2, 3], &(&1 * 2))                   # [2, 4, 6]  (capture)

# each returns :ok (side effects only):
Enum.each([1, 2, 3], fn x -> IO.puts(x) end)
# prints 1, 2, 3; returns :ok

# with index:
Enum.with_index([:a, :b, :c])   # [{:a, 0}, {:b, 1}, {:c, 2}]

# over a map (yields {key, value} pairs):
Enum.map(%{a: 1, b: 2}, fn {k, v} -> {k, v * 10} end)
# [a: 10, b: 20]

접근과 업데이트

. 구문은 atom 키가 필요합니다

elixir
Enum.filter([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)   # [2, 4]
Enum.reject([1, 2, 3, 4, 5], fn x -> rem(x, 2) == 0 end)   # [1, 3, 5]

# truthy filter (drop nil/false):
Enum.filter([nil, 1, false, 2], & &1)   # [1, 2]

# filter + map in one pass via flat_map:
Enum.flat_map([1, 2, 3], fn x ->
  if x > 1, do: [x * 10], else: []
end)
# [20, 30]

Map 모듈

Map 모듈은 연산 함수를 제공합니다

elixir
# sum:
Enum.reduce([1, 2, 3, 4], 0, fn x, acc -> acc + x end)   # 10

# shorthand with a captured operator:
Enum.reduce([1, 2, 3, 4], 0, &+/2)   # 10

# build a map:
Enum.reduce([{:a, 1}, {:b, 2}], %{}, fn {k, v}, acc ->
  Map.put(acc, k, v * 2)
end)
# %{a: 2, b: 4}

# without an initial acc: uses the first element (raises on empty):
Enum.reduce([1, 2, 3], fn x, acc -> x + acc end)   # 6

Structs

defstruct는 구조체를 정의합니다

elixir
Enum.find([1, 2, 3, 4], fn x -> x > 2 end)        # 3 (first match)
Enum.find([1, 2, 3], fn x -> x > 5 end)             # nil (no match)

# find_value returns the FUNCTION result, not the element:
Enum.find_value([1, 2, 3], fn x -> x > 2 && x * 10 end)   # 30

# with a default:
Enum.find([1, 2, 3], :none, fn x -> x > 5 end)      # :none

# find_index:
Enum.find_index([:a, :b, :c], &(&1 == :b))   # 1

# existence checks (short-circuit):
Enum.any?([1, 2, 3], &(&1 > 2))   # true
Enum.all?([1, 2, 3], &(&1 > 0))   # true

sort and sort_by

sort/1 ascending; sort/2 with :desc or a comparator. sort_by/2 extracts a key per element and sorts—cleaner than a custom comparator for structured data. min/max raise on empty lists.

elixir
Enum.sort([3, 1, 2])                # [1, 2, 3]
Enum.sort([3, 1, 2], :desc)         # [3, 2, 1]

# custom comparator:
Enum.sort(["aa", "b", "ccc"], fn a, b -> String.length(a) <= String.length(b) end)
# ["b", "aa", "ccc"]

# sort_by (extract a key per element, sort by it):
Enum.sort_by([%{n: 3}, %{n: 1}, %{n: 2}], & &1.n)
# [%{n: 1}, %{n: 2}, %{n: 3}]

# min / max / min_max:
Enum.min([3, 1, 2])     # 1
Enum.max([3, 1, 2])     # 3
Enum.min_max([3, 1, 2]) # {1, 3}

group_by and count

group_by/2 buckets elements by a key function into a map. count/1 counts all, count/2 counts matches. chunk_every splits by size, chunk_by by a changing key. uniq/1 removes duplicates (preserves order).

elixir
# group_by buckets elements by a key function:
Enum.group_by([1, 2, 3, 4, 5, 6], fn x -> rem(x, 3) end)
# %{0 => [3, 6], 1 => [1, 4], 2 => [2, 5]}

# group structs by a field:
Enum.group_by([%{t: :a}, %{t: :b}, %{t: :a}], & &1.t)

# count:
Enum.count([1, 2, 3])                          # 3
Enum.count([1, 2, 3, 4], fn x -> x > 2 end)    # 2

# chunk (split into groups):
Enum.chunk_every([1, 2, 3, 4, 5], 2)   # [[1, 2], [3, 4], [5]]
Enum.chunk_by([1, 1, 2, 2, 3], & &1)   # [[1, 1], [2, 2], [3]]

# uniq:
Enum.uniq([1, 1, 2, 3, 3])   # [1, 2, 3]
11

문자열 함수

문자열 연산

String 모듈은 UTF-8 문자열을 연산합니다

elixir
# Stream is lazy—operations compose, nothing runs until consumed:
stream = Stream.map([1, 2, 3], fn x -> x * 2 end)
# #Stream<[enum: 1..3, funs: [...]]>  (no computation yet)

# consume to force evaluation:
Enum.to_list(stream)    # [2, 4, 6]
Enum.take(stream, 2)    # [2, 4]

# pipeline of lazy ops:
[1, 2, 3]
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.to_list()       # [4, 6]

분할과 결합

split과 join

elixir
# Stream.iterate(start, next) - infinite:
Stream.iterate(1, &(&1 + 1))
|> Enum.take(5)   # [1, 2, 3, 4, 5]

# Stream.repeatedly(fun) - infinite calls:
Stream.repeatedly(fn -> :rand.uniform(10) end)
|> Enum.take(3)   # e.g. [4, 7, 2]

# Stream.unfold(state, fun) - stateful infinite:
Stream.unfold(0, fn n -> {n, n + 1} end)
|> Enum.take(4)   # [0, 1, 2, 3]

# fibonacci:
Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
|> Enum.take(8)   # [0, 1, 1, 2, 3, 5, 8, 13]

포함과 교체

포함 검사와 교체

elixir
# Stream.cycle repeats an enumerable forever:
Stream.cycle([:a, :b, :c])
|> Enum.take(7)   # [:a, :b, :c, :a, :b, :c, :a]

# alternate a pattern:
Stream.cycle([true, false])
|> Enum.take(5)   # [true, false, true, false, true]

# zip a finite list with a repeating pattern:
Enum.zip([1, 2, 3, 4, 5], Stream.cycle([:x, :y]))
# [{1, :x}, {2, :y}, {3, :x}, {4, :y}, {5, :x}]

보간

#{} 문자열 보간

elixir
# Stream.resource(open, next, close) manages external resources:
stream = Stream.resource(
  fn -> File.open!("data.txt") end,           # open once
  fn file ->
    case IO.read(file, :line) do
      :eof -> {:halt, file}
      line -> {[line], file}
    end
  end,
  fn file -> File.close(file) end             # close always
)

# consume line by line - the file closes when done:
stream |> Enum.take(3)

바이너리와 문자 리스트

이중 따옴표는 바이너리, 단일 따옴표는 charlist입니다

elixir
# Enum: eager, returns a new collection, runs immediately:
1..1_000_000 |> Enum.map(&(&1 * 2)) |> Enum.filter(&(&1 > 2))
# builds two large intermediate lists

# Stream: lazy, composes, runs once when consumed:
1..1_000_000
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 2))
|> Enum.take(5)   # only computes until 5 are found

# rule of thumb:
# - small/finite data, need it now      -> Enum
# - huge/infinite data, or early exit   -> Stream
12

파이프 연산자

기본 파이프

|>는 왼쪽 값을 오른쪽 함수의 첫 번째 인자로 전달합니다

elixir
# define with fn ... end:
add = fn a, b -> a + b end
add.(1, 2)   # 3

# shorthand capture:
square = &(&1 * &1)
square.(5)   # 25

# capture a named function:
upcase = &String.upcase/1
upcase.("hi")   # "HI"

# multi-clause anonymous functions:
greet = fn
  :morning -> "Good morning"
  :evening -> "Good evening"
  _        -> "Hello"
end
greet.(:morning)   # "Good morning"

다단계 파이프

체인 연산이 더 명확합니다

elixir
defmodule Math do
  # public:
  def square(x), do: x * x

  # private (only callable within the module):
  defp secret, do: 42

  # multi-clause with guards:
  def sign(n) when n > 0, do: 1
  def sign(n) when n < 0, do: -1
  def sign(0), do: 0
end

Math.square(4)   # 16
Math.sign(-3)    # -1
# Math.secret()  # UndefinedFunctionError (private)

튜플과 함께

먼저 비구조화 후 파이프

elixir
# capture a named function by module/name/arity:
upcase = &String.upcase/1
upcase.("hi")   # "HI"

# capture a local function:
defmodule M do
  def double(x), do: x * 2
  def quad(x), do: double(x) |> double()
end

# partial application with the capture shorthand:
rem_10 = &rem(&1, 10)
rem_10.(23)   # 3

# operators are functions too:
add = &+/2
add.(3, 4)    # 7

Default Arguments

\\ sets a default value for an argument, evaluated each call. Defaults generate multiple clause heads internally, which can interact subtly with explicit multi-clause functions—define defaults only in a header clause.

elixir
defmodule Greet do
  # \\ sets a default value:
  def hello(name, greeting \\ "Hello", punctuation \\ "!") do
    "#{greeting}, #{name}#{punctuation}"
  end
end

Greet.hello("Alice")                 # "Hello, Alice!"
Greet.hello("Alice", "Hi")           # "Hi, Alice!"
Greet.hello("Alice", "Hey", "?")     # "Hey, Alice?"

# defaults interact with multi-clause functions:
# define defaults only in a header clause (no body),
# then write explicit clauses for each arity.

Multi-clause Functions

A named function can have many clauses (each with its own pattern/guard); the first match wins. Order clauses from most to least specific. The last clause is usually a catch-all to avoid FunctionClauseError.

elixir
defmodule FizzBuzz do
  def fb(n) when rem(n, 15) == 0, do: "FizzBuzz"
  def fb(n) when rem(n, 3) == 0,  do: "Fizz"
  def fb(n) when rem(n, 5) == 0,  do: "Buzz"
  def fb(n), do: to_string(n)
end

Enum.map(1..5, &FizzBuzz.fb/1)
# ["1", "2", "Fizz", "4", "Buzz"]

# clause order matters - first match wins, so put
# specific clauses before the general catch-all.

Closures

Anonymous functions close over variables in scope, capturing them by value at definition time. Rebinding the variable afterward doesn't affect the closure. To share mutable state, use a process (see the Processes section).

elixir
# anonymous functions capture variables from their defining scope:
x = 10
f = fn -> x end
f.()   # 10

# captured by VALUE at definition time:
x = 99
f.()   # still 10 (the old binding)

# closures cannot share mutable state (data is immutable);
# to share state, use a process:
defmodule Counter do
  def start, do: spawn(fn -> loop(0) end)
  defp loop(n) do
    receive do
      {:inc, pid} -> send(pid, {:count, n + 1}); loop(n + 1)
    end
  end
end
13

제어 흐름

cond

cond는 조건을 순서대로 검사합니다

elixir
# |> passes the left value as the FIRST argument to the right call:
"hello" |> String.upcase()    # "HELLO"
# equivalent to: String.upcase("hello")

[1, 2, 3] |> Enum.map(&(&1 * 2))   # [2, 4, 6]
# equivalent to: Enum.map([1, 2, 3], &(&1 * 2))

5 |> Integer.to_string()      # "5"
:ok |> inspect()              # ":ok"

case

case는 패턴 매칭을 기반으로 합니다

elixir
"1,2,3,4,5"
|> String.split(",")
|> Enum.map(&String.to_integer/1)
|> Enum.filter(&(&1 > 2))
|> Enum.sum()
# 12  (3 + 4 + 5)

# without pipes (harder to read, nested inside-out):
Enum.sum(
  Enum.filter(
    Enum.map(String.split("1,2,3,4,5", ","), &String.to_integer/1),
    &(&1 > 2)
  )
)

if와 unless

unless는 if의 부정입니다

elixir
# pipe into functions returning tuples, then destructure:
{:ok, content} =
  "config.json"
  |> File.read!()
  |> Jason.decode!()

# use case when a step might fail:
"file.txt"
|> File.read()
|> case do
  {:ok, body}    -> body
  {:error, _}    -> "default"
end

# tap/2 runs a side effect without breaking the pipe:
[1, 2, 3]
|> Enum.map(&(&1 * 2))
|> tap(&IO.inspect(&1, label: "doubled"))
|> Enum.sum()

with 표현식

with는 패턴 매치를 체인합니다

elixir
# an anonymous function in a pipe must be called with a dot:
[1, 2, 3]
|> (fn list -> Enum.reverse(list) end).()
# [3, 2, 1]

# cleaner: use a named function or capture:
[1, 2, 3] |> Enum.reverse()   # [3, 2, 1]

# then/2 (Elixir 1.12+) applies a function to the piped value:
5 |> then(fn x -> x * x end)   # 25

# useful for inline construction:
def greet(name) do
  name |> then(&"Hi, #{&1}!")
end

Best Practices

Design your own functions data-first (the value as the first argument) so they pipe well. Keep pipelines to a readable length; extract named functions for reuse and tests. Use tap/2 for side effects, not arbitrary prints.

elixir
# DO: one operation per step, readable top-to-bottom:
users
|> Enum.filter(& &1.active)
|> Enum.map(& &1.name)
|> Enum.sort()

# DO: extract long pipelines into named functions for reuse/tests:
def active_names(users) do
  users |> Enum.filter(& &1.active) |> Enum.map(& &1.name)
end

# AVOID: more than ~5 steps in one pipe - split or name intermediates.
# AVOID: mixing side effects into a transform pipeline - use tap/2.
# AVOID: piping into functions whose first arg isn't the data -
#        restructure with a small wrapper.

# design your own functions data-first so they pipe well.
14

재귀

기본 재귀

재귀는 리스트를 처리합니다

elixir
defmodule Greeter do
  @moduledoc "A simple greeting module."

  def hello(name), do: "Hello, #{name}!"
  def goodbye(name), do: "Bye, #{name}!"
end

Greeter.hello("Alice")   # "Hello, Alice!"

# nested modules form a hierarchy:
defmodule MyApp.Net.HTTP do
  def get(url), do: "GET #{url}"
end
MyApp.Net.HTTP.get("/users")

누적기 재귀

꼬리 재귀 최적화 버전

elixir
defmodule Account do
  # public API:
  def balance(user), do: user.balance

  # private helper (not exported):
  defp normalize(name), do: String.downcase(name)

  # one-liner and do/end forms:
  def create(name) do
    %{name: normalize(name), balance: 0}
  end
end

Account.balance(%{balance: 100})   # 100
# Account.normalize("ALICE")       # undefined (private)

트리 재귀

중첩 리스트를 처리합니다

elixir
# bring a module's functions into local scope (no prefix):
defmodule Colors do
  import String
  def shout(s), do: upcase(s) <> "!"   # calls String.upcase
end

# only/except to narrow what's imported:
import Enum, only: [map: 2, filter: 2]
import String, except: [split: 2]

# import only functions or only macros:
import SomeModule, only: :functions
import SomeMacroModule, only: :macros

alias

alias gives a module a shorter local name (default: last segment). Use :as for custom names, and the multi-alias form alias MyApp.{A, B} to group. Aliases are lexical—only visible in the module that declares them.

elixir
# alias creates a short name for a module:
defmodule MyModule do
  alias MyApp.Very.Long.Path.HTTPClient

  def fetch(url) do
    HTTPClient.get(url)   # instead of MyApp.Very.Long.Path.HTTPClient.get
  end
end

# custom alias name with :as:
alias MyApp.Net.HTTPClient, as: HTTP

# multiple aliases at once:
alias MyApp.{Repo, Schema.User}

require

require is needed to invoke a module's macros (which are expanded at compile time). Functions don't need require. Logger.info/1 is a macro, so modules using Logger must require Logger first.

elixir
# require lets you use a module's MACROS (not needed for functions):
defmodule M do
  require Integer

  def even?(n) do
    if Integer.is_even(n), do: :yes, else: :no   # is_even is a macro
  end
end

# Logger is a macro module, so require it first:
require Logger
Logger.info("starting up")

# Kernel is auto-required; its macros (if, unless, def) are always usable.

use

use Module invokes Module's __using__/1 macro at compile time, which can inject any code (callbacks, defaults, requires). It's a flexible extension point—frameworks like GenServer, Phoenix, and Ecto build on it.

elixir
# use invokes the module's __using__/1 macro - a hook for setup:
defmodule MyServer do
  use GenServer          # injects callbacks & defaults
end

# implementing __using__/1 in your own module:
defmodule Tracer do
  defmacro __using__(_opts) do
    quote do
      def traced_call(x), do: IO.inspect(x)
    end
  end
end

defmodule Demo do
  use Tracer
end
Demo.traced_call(42)   # prints 42

# always check the docs to see what use injects.
15

꼬리 호출 최적화

꼬리 재귀

꼬리 재귀는 스택 공간을 소비하지 않습니다

elixir
defmodule AppConfig do
  @max_retries 5
  @timeout_ms 5000
  @env Mix.env()    # evaluated at compile time

  def retry_count, do: @max_retries
  def timeout, do: @timeout_ms

  def call do
    if @env == :prod, do: :prod_call, else: :dev_call
  end
end

비꼬리 재귀

곱셈이 재귀 호출 후에 있어 꼬리 재귀가 아닙니다

elixir
defmodule Greeter do
  @moduledoc """
  Greets people.

  Use `hello/1` to say hello.
  """

  @doc "Says hello to `name`."
  @spec hello(String.t()) :: String.t()
  def hello(name), do: "Hello, #{name}!"

  @doc false   # hide from docs
  def helper, do: :ok
end

# in IEx: h Greeter, h Greeter.hello

꼬리 재귀 팩토리얼

꼬리 재귀를 위해 누적기를 사용하세요

elixir
defmodule Plugin do
  @callbacks []

  # accumulate by re-reading and prepending:
  defmacro register(name) do
    quote do
      @callbacks [unquote(name) | @callbacks]
    end
  end

  register(:init)
  register(:start)

  def all, do: @callbacks
end

Plugin.all()   # [:start, :init]

@behaviour

@behaviour Module declares that this module implements another module's @callback specs (an interface). @impl true marks a function as implementing a callback and lets the compiler check arity/return type. Missing callbacks produce compile-time warnings.

elixir
# a behaviour defines callbacks other modules should implement:
defmodule Parser do
  @doc "Parses input into a data structure."
  @callback parse(String.t()) :: {:ok, term()} | {:error, term()}
end

defmodule JSONParser do
  @behaviour Parser

  @impl true
  def parse(str), do: Jason.decode(str)
end

# if a callback is missing, the compiler warns.

@impl and @callback

@impl true (or @impl GenServer) marks a function as a callback implementation, enabling the compiler to verify it matches a known callback and to warn about typos. Use @callback to declare an interface; @impl to fulfil it.

elixir
defmodule MyGenServer do
  use GenServer

  # @impl marks callback implementations:
  @impl true
  def init(state), do: {:ok, state}

  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}

  # without @impl, you get a warning if the function
  # doesn't override a known callback.
end

# defining your own callbacks:
defmodule Sorter do
  @callback compare(a :: term(), b :: term()) :: boolean()
end
16

프로세스

프로세스 생성

spawn은 경량 프로세스를 생성합니다

elixir
defmodule MyList do
  # base case + recursive case:
  def sum([]), do: 0
  def sum([head | tail]), do: head + sum(tail)

  def len([]), do: 0
  def len([_ | tail]), do: 1 + len(tail)
end

MyList.sum([1, 2, 3])    # 6
MyList.len([:a, :b])     # 2

# recursion replaces loops; pattern matching handles the base case.

메시지 전송

send/receive로 프로세스 간 통신

elixir
# tail-recursive: the recursive call is the LAST operation,
# so BEAM reuses the stack frame (no growth):
defmodule MyList do
  def sum(list), do: sum(list, 0)

  # accumulator carries the running total:
  defp sum([], acc), do: acc
  defp sum([head | tail], acc), do: sum(tail, acc + head)
end

MyList.sum([1, 2, 3, 4])   # 10

# contrast: head + sum(tail) is NOT tail-recursive
# (the + waits for the recursive result).

receive 타임아웃

after는 타임아웃을 설정합니다

elixir
defmodule MyList do
  # reverse using an accumulator (tail-recursive):
  def reverse(list), do: reverse(list, [])
  defp reverse([], acc), do: acc
  defp reverse([h | t], acc), do: reverse(t, [h | acc])

  # map with accumulator (built reversed, then reversed back):
  def map(list, fun), do: map(list, fun, []) |> reverse()
  defp map([], _fun, acc), do: acc
  defp map([h | t], fun, acc), do: map(t, fun, [fun.(h) | acc])
end

MyList.reverse([1, 2, 3])   # [3, 2, 1]

프로세스 연결

spawn_link는 프로세스를 연결하고 함께 종료합니다

elixir
defmodule Deep do
  # count leaves of a nested list:
  def count_leaves([]), do: 0
  def count_leaves([head | tail]) when is_list(head) do
    count_leaves(head) + count_leaves(tail)
  end
  def count_leaves([_head | tail]), do: 1 + count_leaves(tail)

  # flatten any nesting:
  def flatten([]), do: []
  def flatten([h | t]) when is_list(h), do: flatten(h) ++ flatten(t)
  def flatten([h | t]), do: [h | flatten(t)]
end

Deep.count_leaves([1, [2, [3]], 4])   # 4

프로세스 상태

프로세스 상태를 확인합니다

elixir
defmodule MyList do
  # recursive map:
  def map([], _fun), do: []
  def map([head | tail], fun), do: [fun.(head) | map(tail, fun)]

  # recursive filter:
  def filter([], _fun), do: []
  def filter([h | t], fun) do
    if fun.(h), do: [h | filter(t, fun)], else: filter(t, fun)
  end
end

MyList.map([1, 2, 3], &(&1 * 2))                     # [2, 4, 6]
MyList.filter([1, 2, 3, 4], &(rem(&1, 2) == 0))      # [2, 4]

Recursive Reduce

reduce threads an accumulator through recursive calls—it's tail-recursive and handles large lists efficiently. This is essentially how Enum.reduce is implemented. The accumulator holds the running result.

elixir
defmodule MyList do
  # left fold, recursive and tail-recursive:
  def reduce([], acc, _fun), do: acc
  def reduce([h | t], acc, fun), do: reduce(t, fun.(h, acc), fun)
end

MyList.reduce([1, 2, 3, 4], 0, &+/2)   # 10

# the recursive call is the LAST operation,
# so this runs in constant stack space even for huge lists.
# This is essentially how Enum.reduce is implemented.
17

메시지 전달

전송과 수신

패턴 매칭된 메시지 수신

elixir
# spawn/1 creates a lightweight BEAM process:
pid = spawn(fn -> IO.puts("hello from process") end)
# prints "hello from process"; the process is now dead

# spawn/3 with module/function/args:
pid = spawn(Enum, :map, [[1, 2, 3], &(&1 * 2)])

# process info:
Process.alive?(pid)   # false (already finished)
self()                # current process PID

루프 수신

지속적 수신을 위한 재귀

elixir
# send/2 puts a message in a pid's mailbox:
pid = spawn(fn ->
  receive do
    {:hello, from} -> send(from, {:hi, self()})
    {:bye, _}      -> :ok
  end
end)

send(pid, {:hello, self()})

# receive in the current process:
receive do
  {:hi, pid} -> IO.puts("got hi from #{inspect(pid)}")
after
  1000 -> :timeout
end

선택적 수신

after 0은 비차단 검사를 구현합니다

elixir
# a process that keeps state via tail recursion:
defmodule Counter do
  def start(n), do: spawn(fn -> loop(n) end)

  defp loop(state) do
    receive do
      {:inc, by} -> loop(state + by)
      {:get, from} ->
        send(from, {:count, state})
        loop(state)
      :stop -> :ok
    end
  end
end

c = Counter.start(0)
send(c, {:inc, 5})
send(c, {:get, self()})
receive do {:count, n} -> n end   # 5

Links and Termination

spawn_link/1 links two processes: if one exits abnormally, the other does too (Elixir's 'let it crash' philosophy). Set Process.flag(:trap_exit, true) to convert exits into {:EXIT, pid, reason} messages instead of dying.

elixir
# spawn_link: linked processes die together (fault tolerance):
pid = spawn_link(fn ->
  receive do
    :boom -> raise "crash"
  end
end)

send(pid, :boom)
# BOTH processes die (the linked one raises, this one exits too)

# trap exits to handle instead of dying:
Process.flag(:trap_exit, true)
spawn_link(fn -> exit(:kaboom) end)

receive do
  {:EXIT, from, reason} -> IO.puts("#{inspect(from)} died: #{reason}")
end

Monitors

Process.monitor/1 watches a process one-way: when it dies, you get {:DOWN, ref, :process, pid, reason} without dying yourself. Use monitors when you want to observe without coupling lifecycles. Demonitor with Process.demonitor/1.

elixir
# monitors are one-way and don't kill the watcher:
pid = spawn(fn -> exit(:boom) end)
ref = Process.monitor(pid)

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    IO.puts("monitored process died: #{reason}")
end
# prints "monitored process died: boom"

# unlike links, the current process keeps running.
# monitors are asymmetric and unidirectional.
Process.demonitor(ref)   # stop watching early

Timeouts

The after clause in receive sets a timeout in ms; after 0 does a non-blocking check (returns immediately if no match). This enables polling, flush loops, and timeouts. Timeouts are crucial to avoid hanging forever.

elixir
# receive with after for a timeout:
receive do
  {:data, x} -> x
after
  1000 -> :no_response   # 1 second
end

# after 0 = non-blocking check:
receive do
  msg -> {:got, msg}
after
  0 -> :empty
end

# flush helper (drain the mailbox):
def flush do
  receive do
    _ -> flush()
  after
    0 -> :ok
  end
end
18

GenServer

GenServer 정의

use GenServer는 behaviour를 가져옵니다

elixir
defmodule Counter do
  use GenServer

  # callback: initial state
  @impl true
  def init(initial), do: {:ok, initial}

  # synchronous (call) - returns a reply
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}

  # asynchronous (cast) - no reply
  @impl true
  def handle_cast({:inc, n}, state), do: {:noreply, state + n}
end

call 처리

handle_call은 동기식 요청을 처리합니다

elixir
# start_link links the server to the caller (named or unnamed):
{:ok, pid} = GenServer.start_link(Counter, 0, name: MyCounter)

# register via the module name:
{:ok, pid} = GenServer.start_link(Counter, 0, name: __MODULE__)

# starting under a Supervisor (typical in real apps):
children = [
  {Counter, 0}
]
Supervisor.start_link(children, strategy: :one_for_one)

cast 처리

handle_cast는 비동기식 요청을 처리합니다

elixir
@impl true
def handle_call(:get, _from, state) do
  {:reply, state, state}
end

@impl true
def handle_call({:add, n}, _from, state) do
  new_state = state + n
  {:reply, new_state, new_state}
end

# reply tuples:
#   {:reply, reply, new_state}
#   {:reply, reply, new_state, timeout | :hibernate}
#   {:noreply, new_state}        # reply later with GenServer.reply/2
#   {:stop, reason, reply, new_state}

클라이언트 API

클라이언트 인터페이스를 래핑합니다

elixir
@impl true
def handle_cast({:inc, n}, state) do
  {:noreply, state + n}
end

@impl true
def handle_cast(:reset, _state) do
  {:noreply, 0}
end

# cast returns immediately - the caller doesn't wait for a reply.
# useful for fire-and-forget updates, logging, side effects.

# return tuples:
#   {:noreply, new_state}
#   {:noreply, new_state, timeout | :hibernate}
#   {:stop, reason, new_state}

Client API

Convention: put the client API (functions calling GenServer.call/cast) in the same module as the server callbacks. Callers use Counter.get()/inc(), unaware of call/cast details. This hides the messaging behind a clean function interface.

elixir
defmodule Counter do
  use GenServer

  # client API (called by other code):
  def start_link(initial) do
    GenServer.start_link(__MODULE__, initial, name: __MODULE__)
  end
  def get, do: GenServer.call(__MODULE__, :get)
  def inc(n \\ 1), do: GenServer.cast(__MODULE__, {:inc, n})

  # server callbacks:
  @impl true
  def init(initial), do: {:ok, initial}
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}
  @impl true
  def handle_cast({:inc, n}, state), do: {:noreply, state + n}
end

handle_info

handle_info/2 handles 'raw' messages—plain sends, :DOWN monitor notifications, and Process.send_after/3 timeouts. call and cast have their own handlers; everything else lands here. Always implement it to avoid mailbox buildup.

elixir
defmodule Heartbeat do
  use GenServer

  @impl true
  def init(_) do
    send(self(), :tick)
    {:ok, 0}
  end

  # handle non-call/cast messages (plain sends, monitors, timeouts):
  @impl true
  def handle_info(:tick, count) do
    IO.puts("tick ##{count}")
    Process.send_after(self(), :tick, 1000)   # schedule next
    {:noreply, count + 1}
  end

  def handle_info({:DOWN, _ref, :process, pid, reason}, state) do
    IO.puts("#{inspect(pid)} died: #{reason}")
    {:noreply, state}
  end
end
19

Supervisor

Supervisor 정의

Supervisor는 자식 프로세스를 관리합니다

elixir
# generate a new project:
$ mix new my_app
# creates:
#   my_app/
#     lib/my_app.ex
#     lib/my_app/application.ex
#     test/my_app_test.exs
#     mix.exs
#     .formatter.exs
#     README.md

# with a supervision tree (OTP app):
$ mix new my_app --sup

# inside an umbrella project:
$ mix new apps/child_app

재시작 전략

세 가지 재시작 전략

elixir
defp deps do
  [
    {:phoenix, "~> 1.7"},
    {:ecto_sql, "~> 3.10"},
    {:jason, "~> 1.4"},
    {:ex_doc, "~> 0.30", only: :dev, runtime: false},
    {:credo, "~> 1.7", only: [:dev, :test], runtime: false}
  ]
end

# install:
$ mix deps.get

# list the dependency tree:
$ mix deps.tree

# why is a dependency included?
$ mix deps.unlock --unused

자식 스펙

전체 자식 스펙

elixir
$ mix new app        # scaffold a project
$ mix deps.get       # fetch dependencies
$ mix deps.compile   # compile dependencies
$ mix compile        # compile the project
$ mix run            # start the app and keep it running
$ mix run -e "MyApp.hello()"   # run an expression
$ iex -S mix         # IEx with the project loaded
$ mix test           # run the test suite
$ mix format         # format source files
$ mix phx.server     # (Phoenix) start the web server
$ mix help           # list all available tasks

Mix Tasks

Custom tasks live in Mix.Tasks.* modules and use Mix.Task. The module name (after Mix.Tasks.) becomes the command (dots become underscores). @shortdoc shows up in mix help. Mix.shell() provides user I/O.

elixir
# define a custom task module:
defmodule Mix.Tasks.MyApp.Hello do
  use Mix.Task

  @shortdoc "Says hello"

  def run(_args) do
    Mix.shell().info("Hello from my task!")
  end
end

# run it:
$ mix my_app.hello
# Hello from my task!

Environments

Mix has three environments: :dev (default), :test, :prod. Mix.env() returns the current one. config/{env}.exs overrides config/config.exs per environment. Set MIX_ENV at build time (e.g., MIX_ENV=prod mix phx.server).

elixir
# mix.exs uses Mix.env() to branch per environment:
def project do
  [
    app: :my_app,
    version: "0.1.0",
    elixir: "~> 1.15",
    start_permanent: Mix.env() == :prod,   # permanent in prod
    deps: deps()
  ]
end

# config/config.exs:
import Config
config :my_app, key: :default

# config/dev.exs, config/prod.exs, config/test.exs override per env.
# select env at build time:
#   MIX_ENV=prod mix compile
#   MIX_ENV=prod mix phx.server
20

Agents

Agent 생성

Agent는 상태를 캡슐화합니다

elixir
# test/my_app_test.exs
defmodule MyAppTest do
  use ExUnit.Case, async: true

  test "addition" do
    assert 1 + 1 == 2
  end

  test "lists have a head" do
    [head | _] = [1, 2, 3]
    assert head == 1
  end

  test "raises on missing key" do
    assert_raise KeyError, fn -> %{a: 1}.b end
  end
end

# run: mix test

읽기와 업데이트

update로 업데이트, get으로 읽기

elixir
defmodule UserTest do
  use ExUnit.Case

  describe "name validation" do
    test "requires a name" do
      assert User.new(%{}) |> valid?() == false
    end

    test "accepts a non-empty name" do
      assert User.new(%{name: "A"}) |> valid?() == true
    end
  end

  describe "age" do
    test "must be positive" do
      assert {:error, _} = User.new(%{age: -1})
    end
  end
end

간단한 캐시

Agent로 간단한 캐시를 구현합니다

elixir
defmodule DbTest do
  use ExUnit.Case, async: false

  # runs once before each test; context is merged into the test:
  setup do
    conn = open_conn()
    on_exit(fn -> close_conn(conn) end)
    {:ok, conn: conn}
  end

  # runs once for the whole module:
  setup_all do
    {:ok, schema: create_schema()}
  end

  test "uses conn", %{conn: conn} do
    assert query(conn, "SELECT 1") == 1
  end
end

Assertions

assert checks its argument is truthy (with great error output on failure). refute is the inverse. assert_in_delta compares floats within a tolerance. Pattern assertions (assert {:ok, v} = ...) both check and bind.

elixir
assert 1 + 1 == 2
refute 1 == 2                          # opposite of assert
assert_in_delta 3.14, 3.1415, 0.01     # floats within tolerance
assert_raise ArgumentError, fn -> hd([]) end

# pattern assertions (both check and bind):
assert {:ok, val} = parse("1")
assert %User{name: "A"} = build_user()

# catch exits/throws:
assert catch_exit(exit(:boom)) == :boom

# enum/map assertions:
assert length([1, 2]) == 2
assert user.age > 18

Async Tests

async: true runs the test module concurrently with others—safe when tests don't share mutable state. start_supervised/1 starts a process tied to the test's lifecycle and tears it down automatically. Default to async: true; disable only when needed.

elixir
defmodule CounterTest do
  use ExUnit.Case, async: true   # concurrent with other modules

  # each test gets its own Counter process, so no shared state:
  setup do
    {:ok, pid} = start_supervised(Counter)
    %{pid: pid}
  end

  test "increments", %{pid: pid} do
    Counter.inc(pid)
    assert Counter.get(pid) == 1
  end

  test "starts at zero", %{pid: pid} do
    assert Counter.get(pid) == 0
  end
end

Was this helpful?