Elixir untuk Technical Leader — Bab 2 dari 10

Tipe Data & Pattern Matching

Setelah bab ini, kamu bisa baca 90% code Elixir. Serius.


Overview — 7 Hal yang Perlu Kamu Kenal

Elixir cuma punya sedikit tipe data. Ini keunggulan — lebih sedikit yang perlu diingat.

#TipeContohDi LabaBersih
1Integer42, 6_300Quantity, stok, retry_count
2Float / Decimal3.14, Decimal.new("45000")Harga, HPP, fee (SELALU Decimal untuk uang)
3String"hello", "PS2603-00001"Nama, SKU, nomor pesanan, status
4Atom:ok, :error, trueStatus return, keys, boolean
5List[1, 2, 3]Order items, journal lines
6Tuple{:ok, result}Return value SEMUA function
7Map / Struct%{name: "Budi"}Order, Product, JournalEntry

Mari kita bahas satu-satu dengan contoh dari LabaBersih.


1. Atom — Label yang Gak Pernah Berubah

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)
Buat Hafish Atom itu kayak label rak di gudang. Kamu gak bisa ubah tulisan label — "RAK A" ya "RAK A" selamanya. Bedanya dengan string: string bisa dimanipulasi (potong, gabung, ubah huruf besar), atom GAK BISA. Atom cuma untuk identifikasi.

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.


2. Tuple — Paket Hasil (Ini yang Paling Sering Kamu Lihat)

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}}
Ini aturan emas LabaBersih {: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
Red Flag Kalau kamu lihat code yang TIDAK handle {:error, _} — itu bug. Contoh:

{:ok, order} = Orders.ship_order(id, email)

Code ini CRASH kalau function return {:error, _}. Harus pakai case dan handle kedua case.

3. Map — Data dengan Nama (Kayak JSON Object)

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{}

Update map (ingat: immutable!)

# BUKAN mengubah order, tapi bikin SALINAN BARU:
updated = %{order | status: "dikirim"}

# order asli tetap "dikemas"
# updated = salinan dengan status "dikirim"
Analogi Map itu kayak formulir order. Ada field Nama, Telepon, Total, Status. Kalau mau ubah status, kamu gak coret formulir lama — kamu fotokopi formulir itu dan tulis status baru di fotokopian. Formulir asli gak dicoret.

4. List — Kumpulan Data (Kayak Array)

# 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"]

List Pattern yang sering muncul di LabaBersih:

# 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
Red Flag dari LSP tadi LSP detect warning: 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.

5. String — Tulisan + Interpolasi

# 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.


6. Pattern Matching — SKILL TERPENTING

Ini yang bikin Elixir fundamentally berbeda dari bahasa lain. Di JavaScript, = artinya "assign" (masukkan nilai). Di Elixir, = artinya "match" (cocokkan pola).

Level 1: Destructuring (Bongkar data)

# 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]

Level 2: Multi-clause Functions (Ini yang bikin readable)

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
Kenapa ini powerful untuk kamu Kamu bisa BACA business rules langsung dari code. Gak perlu buka dokumentasi terpisah. Code IS the documentation. Kalau kamu lihat function kayak gini, kamu langsung tau semua status transition yang valid.

Level 3: Case + Pattern Matching

# 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

Level 4: Pattern Match di Function Parameters

# 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"

7. Underscore (_) — "Saya Gak Peduli Nilai Ini"

# 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
Dari LSP tadi Warning: 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.

8. Keyword List — Options / Named Arguments

# 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

9. Sigils — Shortcut untuk Data Spesial

# ~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

Baca Code Asli LabaBersih

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):

  1. def ship_order(order_id, actor_email) — function bernama ship_order, terima 2 parameter
  2. order = get_order!(order_id) — ambil order dari database (tanda ! = crash kalau gak ketemu)
  3. case order.status do — cek status order
  4. "dikirim" -> — kalau sudah dikirim: return {:ok, :already_shipped} (idempotent)
  5. "dikemas" -> — kalau dikemas: lanjut proses ship (do_ship_order)
  6. status -> — status lain apapun: return error
Kamu baru saja baca Elixir! Perhatikan: gak ada if, 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."

Cheat Sheet — Simbol yang Sering Muncul

SimbolArtiContoh
: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, _}
_varUnused variable_reason
!Raise if errorget_order!(id)
?Returns booleanString.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...endBlockdef foo do ... end
whenGuard clausedef foo(x) when x > 0
inMembership checkstatus in ["a", "b"]

Checklist Review — Pertanyaan Bab 2

PertanyaanJawaban yang benarRed 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

Ringkasan Bab 2

Yang kamu sekarang bisa:

1. Kenali 7 tipe data — atom, tuple, map, list, string, integer, decimal
2. Baca {:ok, _} / {:error, _} — pattern return SETIAP function LabaBersih
3. Baca case...do...end — pattern matching untuk handle berbagai kondisi
4. Baca multi-clause function — 1 function, banyak versi berdasarkan input
5. Baca simbol|>, %{}, :atom, _, !, ?, ~w(), @
6. Detect red flags — unhandled error, unused variables, length() abuse