기본
Hello World
IO.puts는 줄바꿈과 함께 출력합니다
# 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]주석
단일 줄 주석만 가능합니다
# 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의 대화형 셸입니다
# 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()불변 데이터
데이터는 불변이며, 연산은 새 값을 반환합니다
# 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 twiceAtoms
Atom은 상수이며, 콜론으로 시작합니다
# 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패턴 매칭
매치 연산자
=는 할당이 아닌 매치입니다
# 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"매치 무시
_는 원하지 않는 값을 무시합니다
# 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함수에서 패턴 매칭
여러 절이 순서대로 매치됩니다
# 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') # truecase 표현식
case는 패턴 매칭을 수행합니다
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은 가드 조건을 추가합니다
# 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}불변 데이터
불변 리스트
모든 연산은 새 데이터를 반환합니다
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을 반환합니다
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참조 동등성
참조가 아닌 값을 비교합니다
# 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.
"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.
# = 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 2Membership & 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).
# 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"모듈
모듈 정의
defmodule은 모듈을 정의합니다
# = 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모듈 속성
@는 모듈 속성을 정의합니다
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: falseprivate 함수
defp는 private 함수를 정의합니다
{: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모듈 중첩
모듈은 중첩될 수 있습니다
# 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.
# 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).
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함수
이름 있는 함수
def는 이름 있는 함수를 정의합니다
# 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"기본 인자
\\는 기본값을 설정합니다
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]다중 절 함수
인자를 순서대로 매치합니다
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"}파이프라인 함수
|> 파이프 연산자
# 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 listSharing & 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.
# 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)익명 함수
익명 함수
fn..end는 익명 함수를 정의합니다
[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는 함수를 캡처합니다
[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클로저
익명 함수는 외부 변수를 캡처합니다
# 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"다중 절 익명 함수
익명 함수도 여러 절을 지원합니다
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.
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).
# 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]Enum 모듈
map과 each
map은 새 리스트를 반환, each는 :ok를 반환합니다
{: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}) # 3filter와 reject
filter는 유지, reject는 제외합니다
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는 리스트를 집계합니다
# 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는 함수 결과를 반환합니다
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는 지정된 키로 정렬합니다
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) # 3Stream 모듈
지연 시퀀스
Stream은 지연 평가됩니다
# 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을 생성합니다
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는 무한히 반복합니다
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는 리소스를 관리합니다
# 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 lookupsCommon 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.
# 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)리스트 연산
리스트 기본
리스트는 연결 리스트 구조입니다
# 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연결
++는 연결, --는 제거합니다
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 컴프리헨션은 필터링과 변환을 지원합니다
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 모듈은 리스트 연산을 제공합니다
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.
# 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}
endStructs
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.
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"Map 연산
Map 생성
%{}는 Map을 생성합니다
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 키가 필요합니다
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 모듈은 연산 함수를 제공합니다
# 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) # 6Structs
defstruct는 구조체를 정의합니다
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)) # truesort 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.
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).
# 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]문자열 함수
문자열 연산
String 모듈은 UTF-8 문자열을 연산합니다
# 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
# 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]포함과 교체
포함 검사와 교체
# 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}]보간
#{} 문자열 보간
# 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입니다
# 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파이프 연산자
기본 파이프
|>는 왼쪽 값을 오른쪽 함수의 첫 번째 인자로 전달합니다
# 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"다단계 파이프
체인 연산이 더 명확합니다
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)튜플과 함께
먼저 비구조화 후 파이프
# 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) # 7Default 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.
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.
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).
# 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제어 흐름
cond
cond는 조건을 순서대로 검사합니다
# |> 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는 패턴 매칭을 기반으로 합니다
"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의 부정입니다
# 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는 패턴 매치를 체인합니다
# 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}!")
endBest 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.
# 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.재귀
기본 재귀
재귀는 리스트를 처리합니다
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")누적기 재귀
꼬리 재귀 최적화 버전
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)트리 재귀
중첩 리스트를 처리합니다
# 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: :macrosalias
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.
# 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.
# 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.
# 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.꼬리 호출 최적화
꼬리 재귀
꼬리 재귀는 스택 공간을 소비하지 않습니다
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비꼬리 재귀
곱셈이 재귀 호출 후에 있어 꼬리 재귀가 아닙니다
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꼬리 재귀 팩토리얼
꼬리 재귀를 위해 누적기를 사용하세요
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.
# 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.
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프로세스
프로세스 생성
spawn은 경량 프로세스를 생성합니다
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로 프로세스 간 통신
# 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는 타임아웃을 설정합니다
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는 프로세스를 연결하고 함께 종료합니다
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프로세스 상태
프로세스 상태를 확인합니다
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.
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.메시지 전달
전송과 수신
패턴 매칭된 메시지 수신
# 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루프 수신
지속적 수신을 위한 재귀
# 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은 비차단 검사를 구현합니다
# 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 # 5Links 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.
# 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}")
endMonitors
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.
# 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 earlyTimeouts
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.
# 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
endGenServer
GenServer 정의
use GenServer는 behaviour를 가져옵니다
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}
endcall 처리
handle_call은 동기식 요청을 처리합니다
# 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는 비동기식 요청을 처리합니다
@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
클라이언트 인터페이스를 래핑합니다
@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.
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}
endhandle_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.
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
endSupervisor
Supervisor 정의
Supervisor는 자식 프로세스를 관리합니다
# 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재시작 전략
세 가지 재시작 전략
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자식 스펙
전체 자식 스펙
$ 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 tasksMix 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.
# 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).
# 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.serverAgents
Agent 생성
Agent는 상태를 캡슐화합니다
# 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으로 읽기
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로 간단한 캐시를 구현합니다
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
endAssertions
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.
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 > 18Async 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.
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관련 Elixir 스니펫
Copy-paste ready code for common tasks.
Was this helpful?