Elixir untuk Technical Leader — Bab 2 dari 10
Setelah bab ini, kamu bisa baca 90% code Elixir. Serius.
Elixir cuma punya sedikit tipe data. Ini keunggulan — lebih sedikit yang perlu diingat.
| # | Tipe | Contoh | Di LabaBersih |
|---|---|---|---|
| 1 | Integer | 42, 6_300 | Quantity, stok, retry_count |
| 2 | Float / Decimal | 3.14, Decimal.new("45000") | Harga, HPP, fee (SELALU Decimal untuk uang) |
| 3 | String | "hello", "PS2603-00001" | Nama, SKU, nomor pesanan, status |
| 4 | Atom | :ok, :error, true | Status return, keys, boolean |
| 5 | List | [1, 2, 3] | Order items, journal lines |
| 6 | Tuple | {:ok, result} | Return value SEMUA function |
| 7 | Map / Struct | %{name: "Budi"} | Order, Product, JournalEntry |
Mari kita bahas satu-satu dengan contoh dari LabaBersih.
Atom = label/tag konstan. Ditulis dengan titik dua di depan: :ok, :error, :dibuat.
# Atom di LabaBersih:
:ok # operasi berhasil
:error # operasi gagal
:asc # sort ascending (FEFO: lot paling lama dulu)
true # boolean true (ini juga atom!)
false # boolean false (juga atom)
nil # "gak ada nilai" (juga atom)
Kenapa gak pakai string aja? Karena atom lebih cepat untuk dibandingkan. Komputer tinggal cek "label sama atau beda?" — gak perlu baca karakter satu-satu kayak string.
Tuple = kumpulan nilai dengan ukuran tetap. Ditulis pakai kurung kurawal: {a, b}.
Pattern terpenting di SELURUH codebase LabaBersih:
# SETIAP function di LabaBersih return 1 dari 2:
{:ok, result} # berhasil — result = data yang diminta
{:error, reason} # gagal — reason = kenapa gagal
# Contoh nyata:
Orders.ship_order(order_id, email)
# → {:ok, %{order: order, journal: journal}}
# → {:error, :already_shipped}
# → {:error, {:insufficient_stock, "Forbest", 3, 5}}
{:ok, result} atau {:error, reason}. Selalu. Gak ada exception. Gak ada silent failure. Caller HARUS handle kedua case. Ini yang bikin code kita predictable.
Cara caller handle-nya:
case Orders.ship_order(order_id, email) do
{:ok, result} ->
# sukses! result berisi order + journal
put_flash(socket, :info, "Pesanan berhasil dikirim")
{:error, :already_shipped} ->
# order sudah dikirim sebelumnya (idempotent, bukan error fatal)
put_flash(socket, :info, "Pesanan sudah dikirim sebelumnya")
{:error, reason} ->
# error lainnya — tampilkan pesan ke user
put_flash(socket, :error, "Gagal: #{inspect(reason)}")
end
{:error, _} — itu bug. Contoh:
{:ok, order} = Orders.ship_order(id, email)
{:error, _}. Harus pakai case dan handle kedua case.
Map = key-value pairs. Kalau kamu kenal JSON, map = JSON object.
# Map biasa:
%{name: "Budi", phone: "081234", total: 200_000}
# Akses value:
order.customer_name # → "Budi"
order.total_harga # → 200000
# Map di LabaBersih biasanya = Ecto Struct (Bab 4)
# Contoh: %Order{}, %Product{}, %JournalEntry{}
# BUKAN mengubah order, tapi bikin SALINAN BARU:
updated = %{order | status: "dikirim"}
# order asli tetap "dikemas"
# updated = salinan dengan status "dikirim"
# List items di order:
items = [
%{sku: "FRB01", qty: 3, price: 45_000},
%{sku: "RTN01", qty: 1, price: 60_000}
]
# Jumlah items:
length(items) # → 2
# Loop setiap item:
Enum.each(items, fn item ->
IO.puts(item.sku)
end)
# Transform setiap item (kayak .map() di JavaScript):
skus = Enum.map(items, fn item -> item.sku end)
# → ["FRB01", "RTN01"]
# Cek list kosong atau isi:
[] # list kosong
[_ | _] # list yang ada isinya (minimal 1 elemen)
# Contoh nyata di code:
case items do
[] -> {:error, "Order tanpa item"}
[_ | _] -> # ada items, lanjut proses
end
do not use "length(data_rows) > 0". Kenapa? Karena length() harus hitung SEMUA elemen. Kalau list-nya 10.000 item, dia hitung 10.000 elemen cuma buat tau "ada isi atau gak?". Harusnya pakai data_rows != [] atau pattern match [_ | _] — instant, gak perlu hitung.
# String biasa:
name = "LabaBersih"
# String interpolation (masukkan variabel ke dalam string):
message = "Pesanan #{order.id} berhasil dikirim"
# → "Pesanan PS2603-00001 berhasil dikirim"
# Multi-line string:
description = """
Penjualan TikTok Bestari
Tanggal: 30 Mar 2026
Total: Rp 200.000
"""
Yang penting: #{...} di dalam string = interpolasi. Isinya di-evaluate dan jadi text.
Ini yang bikin Elixir fundamentally berbeda dari bahasa lain. Di JavaScript, = artinya "assign" (masukkan nilai). Di Elixir, = artinya "match" (cocokkan pola).
# Match tuple — bongkar isi tuple ke variabel:
{:ok, order} = Orders.ship_order(id, email)
# Sekarang variabel `order` berisi data order
# TAPI: kalau return {:error, _} → CRASH (karena :ok gak match :error)
# Match map — ambil field tertentu:
%{customer_name: name, total_harga: total} = order
# Sekarang `name` = "Budi", `total` = 200000
# Match list — ambil elemen pertama:
[first | rest] = [1, 2, 3]
# first = 1, rest = [2, 3]
Ini kekuatan utama. Satu function bisa punya banyak versi, masing-masing handle case berbeda:
# Dari LabaBersih — advance_fulfillment_status:
def advance_fulfillment_status("unfulfilled"), do: {:ok, "picking"}
def advance_fulfillment_status("picking"), do: {:ok, "packed"}
def advance_fulfillment_status("packed"), do: {:ok, "shipped"}
def advance_fulfillment_status("shipped"), do: {:ok, "in_transit"}
def advance_fulfillment_status("in_transit"), do: {:ok, "delivered"}
def advance_fulfillment_status(status), do: {:error, "#{status} gak bisa maju"}
# Baca dari atas ke bawah — kayak baca SOP gudang:
# "unfulfilled" → boleh maju ke "picking"
# "picking" → boleh maju ke "packed"
# ... dst
# status lain → TOLAK
# ship_order internal — handle berbagai kondisi:
case order.status do
"dikemas" ->
# BOLEH ship — lanjut proses
do_ship(order)
"dikirim" ->
# Sudah dikirim — idempotent, skip (bukan error)
{:ok, :already_shipped}
status when status in ["selesai", "dibatalkan"] ->
# Status final — TOLAK
{:error, {:invalid_status, status}}
other ->
# Status gak dikenal — TOLAK
{:error, {:unknown_status, other}}
end
# Function yang terima map dan langsung bongkar field-nya:
def process_item(%{sku: sku, quantity: qty, price: price}) do
# sku, qty, price langsung tersedia sebagai variabel
# Gak perlu: item.sku, item.quantity, item.price
total = qty * price
end
# Kalau parameter gak punya field `sku` → ERROR saat compile
# Gak perlu cek manual "if item.sku != undefined"
# Underscore = wildcard, match apapun tapi BUANG nilainya
{:ok, _} = some_function()
# Saya cuma mau tau berhasil, gak peduli result-nya
{:error, _reason} = some_function()
# _reason = variabel yang sengaja gak dipakai
# Prefix _ = compiler gak warn "unused variable"
def handle_event("save", _params, socket) do
# Saya tau event "save", gak butuh params-nya
end
variable "warehouse_id" is unused. Fix-nya: rename ke _warehouse_id. Prefix underscore = bilang ke compiler "saya sengaja gak pakai ini." Tanpa underscore, compiler warn karena takut kamu lupa pakai.
# Keyword list = list of {atom, value} tuples
# Sering dipakai sebagai "options" di function call:
list_orders(org_id, status: "dikirim", page: 1, per_page: 20)
# Ini sama dengan:
list_orders(org_id, [{:status, "dikirim"}, {:page, 1}, {:per_page, 20}])
# Versi pendek lebih enak dibaca
# Kamu akan sering lihat pattern ini di LabaBersih
# ~w = word list (bikin list dari kata-kata):
@statuses ~w(dibuat diproses selesai dibatalkan)
# Sama dengan: ["dibuat", "diproses", "selesai", "dibatalkan"]
# Di LabaBersih, ini dipakai untuk define valid statuses:
@order_statuses ~w(dibuat diproses selesai dibatalkan)
@fulfillment_statuses ~w(unfulfilled picking packed shipped in_transit delivered rts_detected hold_requested retrying returned)
# Guna: validate input. Kalau status diluar list → TOLAK
Sekarang coba baca code asli dari orders.ex. Kamu harusnya bisa paham 80%+ setelah bab ini:
def ship_order(order_id, actor_email) do
order = get_order!(order_id)
case order.status do
"dikirim" ->
{:ok, :already_shipped}
"dikemas" ->
do_ship_order(order, actor_email)
status ->
{:error, {:invalid_status, status}}
end
end
Cara bacanya (atas ke bawah):
def ship_order(order_id, actor_email) — function bernama ship_order, terima 2 parameterorder = get_order!(order_id) — ambil order dari database (tanda ! = crash kalau gak ketemu)case order.status do — cek status order"dikirim" -> — kalau sudah dikirim: return {:ok, :already_shipped} (idempotent)"dikemas" -> — kalau dikemas: lanjut proses ship (do_ship_order)status -> — status lain apapun: return errorif, gak ada else, gak ada throw. Cuma pattern matching. Dan kamu bisa paham business rule-nya: "cuma order dikemas yang boleh di-ship, yang sudah dikirim di-skip, sisanya ditolak."
| Simbol | Arti | Contoh |
|---|---|---|
: | Atom prefix | :ok, :error, :asc |
%{} | Map (key-value) | %{name: "Budi"} |
%Struct{} | Struct (typed map) | %Order{status: "dibuat"} |
{} | Tuple | {:ok, result} |
[] | List | [1, 2, 3] |
|> | Pipe (Bab 3) | data |> transform() |> save() |
_ | Wildcard / ignore | {:ok, _} |
_var | Unused variable | _reason |
! | Raise if error | get_order!(id) |
? | Returns boolean | String.contains?(s, "x") |
# | Comment | # ini komentar |
#{} | String interpolation | "Hello #{name}" |
~w() | Word list | ~w(a b c) = ["a","b","c"] |
@ | Module attribute | @statuses ~w(dibuat dikemas) |
-> | Arrow (case/fn body) | "dikemas" -> do_ship() |
do...end | Block | def foo do ... end |
when | Guard clause | def foo(x) when x > 0 |
in | Membership check | status in ["a", "b"] |
| Pertanyaan | Jawaban yang benar | Red flag |
|---|---|---|
| "Function ini return apa?" | "{:ok, data} atau {:error, reason}" |
"Return data langsung tanpa tuple" ← melanggar convention |
| "Ini handle error case-nya?" | "Ya, ada case/pattern match untuk :ok dan :error" | "Pakai {:ok, x} = ... tanpa handle error" ← crash-prone |
| "Kenapa ada underscore di variabel?" | "_var = sengaja gak dipakai, compiler gak warn" | Variabel tanpa underscore tapi gak dipakai ← warning |
"Kenapa pakai case bukan if-else?" |
"Pattern matching lebih expressive + compiler cek exhaustiveness" | "Karena convention aja" ← ada alasan teknis |
|>, %{}, :atom, _, !, ?, ~w(), @