Elixir untuk Technical Leader — Bab 3 dari 10

Functions, Modules, Pipe

Cara kerja "mesin" di balik LabaBersih — dari departemen (module) sampai jalur produksi (pipe).


Overview — Apa yang Akan Kamu Pelajari

Kalau Bab 2 tentang data (bahan baku), bab ini tentang mesin yang mengolah data. Di Elixir, mesin itu terdiri dari:

#KonsepAnalogiContoh LabaBersih
1ModuleDepartemen/divisiLababersih.Orders, Lababersih.Accounting
2FunctionTugas/operasiship_order, create_journal_entry
3Pipe |>Jalur produksi / assembly linequery |> filter() |> sort() |> Repo.all()
4EnumSwiss Army knife untuk listEnum.map, Enum.filter, Enum.reduce

Setelah bab ini, kamu bisa baca SEMUA function di codebase LabaBersih dan paham strukturnya.


1. Module — Departemen di Organisasi

Module = tempat function tinggal. Di LabaBersih, setiap module = 1 domain bisnis:

defmodule Lababersih.Orders do
  # Semua function terkait pesanan tinggal di sini:
  # - create_order
  # - ship_order
  # - list_orders
  # - delete_order
end
Analogi: Departemen di Perusahaan Bayangkan LabaBersih sebagai perusahaan. Setiap defmodule = 1 departemen:

Lababersih.Orders = Tim Pesanan (terima, kirim, batalkan)
Lababersih.Accounting = Tim Akuntansi (jurnal, laporan, tutup buku)
Lababersih.Inventory = Tim Gudang (stok, FEFO, mutasi)
Lababersih.Sales = Tim Toko (channel, fee mapping)
Lababersih.Returns = Tim Retur (RTS, klaim kurir)

Setiap departemen punya tugas sendiri. Gak boleh overlap. Kalau butuh bantuan departemen lain, panggil lewat "pintu resmi" (public function).

Cara manggil function dari module lain:

# Panggil function dari module lain pakai NamaModule.nama_function()
Lababersih.Accounting.create_journal_entry(attrs)
Lababersih.Inventory.consume_lots(product_id, qty)

# Setelah di-alias (shortcut), cukup:
Accounting.create_journal_entry(attrs)
Inventory.consume_lots(product_id, qty)

Struktur File = Struktur Module

Ada aturan 1:1 — setiap file = 1 module:

FileModuleIsinya
lib/lababersih/orders/orders.exLababersih.OrdersContext — business logic
lib/lababersih/orders/order.exLababersih.Orders.OrderSchema — definisi tabel
lib/lababersih/orders/order_item.exLababersih.Orders.OrderItemSchema — line item
Buat Hafish Kalau review code dan ada function tentang stok di orders.ex, itu red flag. Logic stok harusnya di inventory.ex. Setiap module punya tanggung jawab sendiri — ini prinsip yang bikin codebase gak jadi mie goreng.

2. def vs defp — Pintu Depan vs Dapur

Ada 2 jenis function di Elixir:

def = Public (Pintu Depan)
Bisa dipanggil dari module lain.
Ini "API" yang disediakan departemen.

def ship_order(id, email)
defp = Private (Dapur)
Cuma bisa dipanggil dari dalam module sendiri.
Detail internal yang gak perlu diketahui orang luar.

defp do_consume_lots(lots, qty)
defmodule Lababersih.Orders do

  # PUBLIC — bisa dipanggil dari LiveView, test, module lain
  def ship_order(order_id, actor_email) do
    order = get_order!(order_id)
    do_ship_order(order, actor_email)  # panggil function private
  end

  # PRIVATE — cuma bisa dipanggil dari dalam module ini
  defp do_ship_order(order, actor_email) do
    # 1. potong stok
    # 2. generate jurnal
    # 3. update status
    # 4. audit trail
  end

end
Analogi: Restoran def = menu yang bisa dipesan pelanggan ("Nasi Goreng Spesial")
defp = resep di dapur ("potong bawang 2mm, tumis 3 menit pada suhu 180C")

Pelanggan gak perlu tau detail dapur. Mereka cuma perlu tau menu apa yang tersedia. Sama — LiveView gak perlu tau gimana FEFO lot dikonsumsi. Cukup panggil ship_order/2.
Red Flag Kalau ada function defp yang dipanggil dari LiveView atau test — itu salah. Private function gak bisa diakses dari luar. Biasanya artinya: function itu harusnya def, atau ada function public yang hilang.

3. Naming Convention — Nama = Niat

Di LabaBersih, nama function HARUS menjelaskan apa yang dilakukan. Ini bukan sekedar convention — ini kontrak bisnis:

PrefixArtinyaContohCatatan
create_Buat 1 record barucreate_order, create_journal_entryInsert ke database
get_!Ambil 1, CRASH kalau gak adaget_order!, get_product!Tanda seru = bahaya kalau nil
get_Ambil 1, return nil kalau gak adaget_account_by_codeAman, gak crash
list_Ambil banyak (daftar)list_orders, list_productsReturn list, bisa kosong []
update_Update 1 recordupdate_productPartial update
delete_Hapus 1 recorddelete_order+ reverse side effects
process_Operasi multi-step atomicprocess_rts, process_reconciliationRepo.transaction wajib
ship_Kirim (verb spesifik domain)ship_orderStatus + stok + jurnal + audit
void_Batalkan/voidvoid_journal_entrySoft delete, tetap di DB
calculate_Hitung (pure, no side effect)calculate_estimated_feesGak sentuh database
generate_Generate data (pure)generate_sale_journalReturn data, belum save
parse_Parse file/dataparse_settlement_xlsxXLSX, CSV, API response
Buat Hafish: Cara Baca Cepat Lihat nama function = langsung tau apa yang dilakukan:

create_order → "bikin order baru"
get_order! → "ambil 1 order, crash kalau gak ada"
process_reconciliation → "proses rekonsiliasi, multi-step, atomic"
calculate_estimated_fees → "hitung fee, cuma hitung, gak save"

Kalau ada function bernama do_stuff atau handle_datared flag. Nama harus spesifik.

Tanda ! dan ? di Nama Function

# ! = bahaya (raise/crash kalau gagal)
get_order!(id)        # → %Order{} atau CRASH (Ecto.NoResultsError)

# ? = return boolean
String.contains?("hello", "ell")   # → true
Enum.empty?([])                    # → true
Enum.any?(items, &(&1.qty > 0))   # → true/false

4. Pipe Operator |> — Jalur Produksi

Ini fitur Elixir yang paling sering bikin orang jatuh cinta. Pipe = ambil hasil kiri, lempar jadi parameter pertama fungsi kanan.

Tanpa pipe (nested, susah dibaca):

# Baca dari DALAM ke LUAR — pusing:
Repo.preload(Repo.all(offset(limit(where(Order, [o], o.org_id == ^org_id), 20), page * 20)), :items)

Dengan pipe (linear, kayak baca koran):

# Baca dari ATAS ke BAWAH — jelas:
Order
|> where([o], o.org_id == ^org_id)
|> limit(20)
|> offset(page * 20)
|> Repo.all()
|> Repo.preload(:items)
Analogi: Assembly Line di Pabrik Bayangkan pabrik yang bikin produk:

Bahan mentah (Order) → Saring (where: org_id ini) → Potong (limit: 20) → Skip (offset: halaman 2) → Ambil dari gudang (Repo.all) → Tempel label (preload: items)

Setiap stasiun terima hasil dari stasiun sebelumnya. Data mengalir dari atas ke bawah.

Cara pipe bekerja secara teknis:

# x |> f(a, b)  adalah  f(x, a, b)
# Hasil kiri MASUK jadi parameter PERTAMA fungsi kanan

"  hello  "
|> String.trim()           # String.trim("  hello  ") → "hello"
|> String.upcase()         # String.upcase("hello") → "HELLO"
|> String.replace("L", "R") # String.replace("HELLO", "L", "R") → "HERRO"

Pipe di code LabaBersih yang nyata:

# Di Orders context — list orders dengan filter + pagination
def list_orders(org_id, opts \\ []) do
  Order
  |> where([o], o.org_id == ^org_id)
  |> maybe_filter_status(opts[:status])
  |> maybe_filter_platform(opts[:platform])
  |> order_by([o], [desc: o.inserted_at])
  |> limit(^per_page)
  |> offset(^offset)
  |> Repo.all()
  |> Repo.preload(:items)
end
Buat Hafish: Review Pipe Saat review code, pipe harus bisa dibaca dari atas ke bawah kayak narasi:

"Ambil Order → filter org ini → filter status (kalau ada) → urutkan terbaru → ambil 20 → skip halaman → eksekusi query → sertakan items."

Kalau kamu baca pipe dan gak bisa ceritakan narasinya — code-nya terlalu kompleks. Minta dipecah.

5. Anonymous Functions — Function Tanpa Nama

Kadang kamu butuh function kecil yang cuma dipakai sekali — gak perlu nama. Ini disebut anonymous function atau lambda.

# Anonymous function — bentuk panjang:
Enum.map(items, fn item -> item.sku end)
# → ["FRB01", "RTN01", "ORG01"]

# Shorthand — pakai & dan &1:
Enum.map(items, &(&1.sku))
# → ["FRB01", "RTN01", "ORG01"]
# &1 = parameter pertama (item)

Cara bacanya:

CodeCara baca
fn item -> item.sku end"untuk setiap item, ambil field sku"
&(&1.sku)Sama persis, cuma lebih pendek
fn item -> item.qty * item.price end"untuk setiap item, kalikan qty dengan price"
&(&1.qty * &1.price)Sama, shorthand. &1 = parameter pertama

Contoh nyata di LabaBersih:

# Ambil semua SKU dari order items:
skus = Enum.map(order.items, &(&1.sku))
# → ["FRB01", "RTN01"]

# Hitung total harga semua items:
total = Enum.reduce(order.items, 0, fn item, acc ->
  acc + item.qty * item.price
end)

# Filter cuma items yang qty > 0:
valid_items = Enum.filter(items, fn item -> item.qty > 0 end)

# Shorthand yang sama:
valid_items = Enum.filter(items, &(&1.qty > 0))
Buat Hafish Kalau lihat &(&1.xxx) dan bingung — ganti di kepala dengan "untuk setiap elemen, ambil field xxx". Itu aja. &1 = "elemen yang sedang diproses".

6. Enum — Swiss Army Knife untuk List

Enum adalah module bawaan Elixir yang berisi function untuk mengolah list. Ini yang paling sering muncul di codebase LabaBersih.

Top 8 Enum yang paling sering dipakai:

FunctionApa yang dilakukanContoh LabaBersih
Enum.map Transform setiap elemen Enum.map(items, &(&1.sku)) → list SKU
Enum.filter Saring yang memenuhi syarat Enum.filter(orders, &(&1.status == "dikirim"))
Enum.each Jalankan sesuatu per elemen (no return) Enum.each(items, fn i -> deduct_stock(i) end)
Enum.reduce Akumulasi jadi 1 nilai Enum.reduce(items, 0, &(&1.total + &2)) → grand total
Enum.find Cari 1 yang pertama cocok Enum.find(lots, &(&1.qty_remaining > 0))
Enum.any? Ada yang cocok? (boolean) Enum.any?(items, &(&1.product_id == nil))
Enum.count Hitung jumlah Enum.count(orders, &(&1.status == "rts"))
Enum.sum Jumlahkan (shortcut reduce) Enum.sum(Enum.map(items, &(&1.qty)))

Contoh lengkap — mengolah data order items:

items = [
  %{sku: "FRB01", qty: 3, price: 45_000},
  %{sku: "RTN01", qty: 2, price: 60_000},
  %{sku: "SAM01", qty: 0, price: 30_000}
]

# Ambil semua SKU:
Enum.map(items, &(&1.sku))
# → ["FRB01", "RTN01", "SAM01"]

# Filter cuma yang qty > 0:
Enum.filter(items, &(&1.qty > 0))
# → [%{sku: "FRB01", ...}, %{sku: "RTN01", ...}]

# Hitung total value (qty × price per item, lalu sum):
items
|> Enum.filter(&(&1.qty > 0))
|> Enum.map(fn i -> i.qty * i.price end)
|> Enum.sum()
# → 255_000 (3×45000 + 2×60000)

# Cek ada item tanpa product_id (SKU unknown)?
Enum.any?(items, &(&1.product_id == nil))
# → true/false
Buat Hafish: Enum = 80% Code Logic Hampir semua business logic di LabaBersih itu variasi dari: "punya list data, olah, hasilkan sesuatu". Enum adalah tools-nya. Kalau kamu paham 5 function (map, filter, reduce, find, each), kamu bisa baca 80% logic.

7. Guards — "Syarat Tambahan" di Function

Guard = syarat ekstra setelah parameter, pakai keyword when:

# Function hanya jalan kalau qty positif:
def reserve_stock(product_id, qty) when qty > 0 do
  # proses reservasi...
end

# Function untuk qty = 0 atau negatif → error:
def reserve_stock(_product_id, qty) when qty <= 0 do
  {:error, :invalid_quantity}
end

Contoh lain di LabaBersih:

# Cek tipe data:
def format_money(amount) when is_number(amount) do
  # format angka jadi "Rp 200.000"
end

# Cek apakah list:
def bulk_create(items) when is_list(items) do
  # proses batch...
end

# Guard umum yang sering muncul:
# when is_binary(x)     — x adalah string
# when is_integer(x)    — x adalah angka bulat
# when is_list(x)       — x adalah list
# when is_nil(x)        — x adalah nil
# when x > 0            — x positif
# when x in [:a, :b]    — x salah satu dari list
Cara Baca Guard def foo(x) when x > 0 dibaca: "function foo yang cuma jalan kalau x lebih dari 0". Kalau x = -1, function ini di-skip dan Elixir cari versi lain.

8. Default Arguments \\

Backslash-backslash \\ = nilai default kalau parameter gak diisi:

# opts punya default [] (list kosong)
def list_orders(org_id, opts \\ []) do
  # opts bisa berisi: status, platform, page, per_page
end

# Bisa dipanggil tanpa opts:
list_orders("org-123")
# → opts = []

# Atau dengan opts:
list_orders("org-123", status: "dikirim", page: 2)
# → opts = [status: "dikirim", page: 2]

Pattern ini SANGAT sering di LabaBersih — hampir semua list_* function pakai opts \\ [] supaya filter opsional.

Buat Hafish Saat review, lihat parameter terakhir opts \\ []. Artinya: "filter ini opsional — gak perlu diisi semua, cuma yang dibutuhkan." Ini bikin function fleksibel tanpa bikin 10 versi berbeda.

9. alias, import, use — Shortcut dan Setup

3 kata kunci ini sering muncul di awal module. Fungsinya:

alias — Shortcut nama module

# Tanpa alias (panjang):
Lababersih.Orders.Order
Lababersih.Accounting.Accounting

# Dengan alias (pendek):
alias Lababersih.Orders.Order
alias Lababersih.Accounting

# Sekarang cukup tulis:
Order        # = Lababersih.Orders.Order
Accounting   # = Lababersih.Accounting

import — Bawa function ke scope

# Tanpa import:
Ecto.Query.where(Order, ...)
Ecto.Query.limit(query, 20)

# Dengan import:
import Ecto.Query

# Sekarang bisa langsung:
where(Order, ...)
limit(query, 20)

use — "Setup" dari module lain

# use = "pakai template/setup dari module ini"
defmodule Lababersih.Orders.Order do
  use Ecto.Schema      # setup untuk jadi database schema
  import Ecto.Changeset # bawa fungsi validasi

  # sekarang bisa pakai schema, field, cast, validate_required, dll
end
KeywordFungsiAnalogi
aliasShortcut namaKontak HP: "Budi Gudang" bukan "+628123456789"
importBawa function masukBawa toolbox ke meja kerja
useInject setup/templatePakai template surat resmi (header + footer otomatis)
Buat review: ketiga keyword ini cuma muncul di bagian ATAS module. Kalau kamu lihat alias di tengah function — aneh (valid tapi unusual). Normalnya di atas, setelah defmodule.

10. @moduledoc dan @doc — Dokumentasi Otomatis

Elixir punya sistem dokumentasi built-in. Tulis sekali → langsung jadi halaman docs:

defmodule Lababersih.Orders do
  @moduledoc """
  Context untuk manajemen pesanan.

  Semua operasi terkait order: create, ship, delete, list.
  Dependency: Inventory (stok), Accounting (jurnal), Sales (toko).
  """

  @doc """
  Kirim pesanan — atomic 5 step.

  1. Validasi status = dikemas
  2. Potong stok (FEFO lot consumption)
  3. Generate jurnal penjualan + HPP
  4. Update status → dikirim
  5. Audit trail

  Returns `{:ok, result}` atau `{:error, reason}`.
  """
  def ship_order(order_id, actor_email) do
    # ...
  end
end
Buat Hafish @moduledoc dan @doc itu BUKAN komentar biasa. Ini jadi halaman HTML saat di-generate pakai ExDoc (mix docs). Jadi siapapun bisa buka browser dan baca dokumentasi tanpa buka code.

Rule LabaBersih: setiap module WAJIB punya @moduledoc. Kalau gak ada → red flag.

11. @module_attribute — Konstanta Compile-Time

@nama di level module = konstanta yang di-set saat compile:

defmodule Lababersih.Orders.Order do
  # Konstanta — gak bisa berubah saat runtime
  @order_statuses ~w(dibuat diproses selesai dibatalkan)
  @fulfillment_statuses ~w(unfulfilled picking packed shipped in_transit
    delivered rts_detected hold_requested retrying returned)
  @payment_statuses ~w(unpaid settled refunded)

  # Dipakai untuk validasi:
  def changeset(order, attrs) do
    order
    |> cast(attrs, [:order_status, :fulfillment_status])
    |> validate_inclusion(:order_status, @order_statuses)
    |> validate_inclusion(:fulfillment_status, @fulfillment_statuses)
  end
end
Analogi Module attribute kayak peraturan perusahaan yang ditulis di tembok kantor. Semua orang bisa lihat, gak ada yang bisa ubah. @order_statuses bilang: "status order yang valid CUMA 4 ini. Titik."

Yang sering muncul di LabaBersih:

AttributeContohFungsi
@moduledoc@moduledoc "Context pesanan"Dokumentasi module
@doc@doc "Kirim pesanan"Dokumentasi function
@statuses@statuses ~w(dibuat dikemas)Valid values untuk validasi
@primary_key@primary_key {:id, :binary_id, ...}Konfigurasi Ecto schema

Baca Code Asli LabaBersih

Sekarang coba baca function nyata. Dengan semua yang kamu pelajari di bab ini, kamu harusnya bisa paham 90%:

defmodule Lababersih.Inventory do
  @moduledoc "Context untuk produk, stok, FEFO lots, warehouse."

  alias Lababersih.Repo
  alias Lababersih.Inventory.Product
  import Ecto.Query

  @doc "List produk per organisasi dengan filter opsional."
  def list_products(org_id, opts \\ []) do
    Product
    |> where([p], p.org_id == ^org_id)
    |> maybe_search(opts[:search])
    |> order_by([p], asc: p.name)
    |> Repo.all()
  end

  defp maybe_search(query, nil), do: query
  defp maybe_search(query, search) do
    search_term = "%#{search}%"
    where(query, [p], ilike(p.name, ^search_term) or ilike(p.sku, ^search_term))
  end
end

Cara bacanya baris per baris:

  1. defmodule Lababersih.Inventory do — module "Departemen Gudang"
  2. @moduledoc "..." — deskripsi module
  3. alias + import — shortcut dan tools yang dibutuhkan
  4. def list_products(org_id, opts \\ []) — function PUBLIC, terima org_id (wajib) dan opts (opsional)
  5. Pipe: Product |> where... |> maybe_search... |> order_by... |> Repo.all() — assembly line query
  6. defp maybe_search(query, nil) — PRIVATE function, kalau search = nil, kembalikan query apa adanya
  7. defp maybe_search(query, search) — PRIVATE function, kalau search ada isinya, tambahkan filter ILIKE
Perhatikan pattern: maybe_search punya 2 versi — ini multi-clause function dari Bab 2! Kalau search = nil, skip. Kalau ada, filter. Gak ada if-else. Elegan.

Cheat Sheet Enum

FunctionInput → OutputContoh
Enum.map(list, fn)[a, b, c] → [f(a), f(b), f(c)]Transform semua elemen
Enum.filter(list, fn)[a, b, c] → subset yang trueSaring yang memenuhi syarat
Enum.reject(list, fn)[a, b, c] → subset yang falseKebalikan filter
Enum.reduce(list, acc, fn)[a, b, c] → 1 nilaiAkumulasi (total, concat, dll)
Enum.each(list, fn)[a, b, c] → :okSide effect per elemen
Enum.find(list, fn)[a, b, c] → elemen / nilCari pertama yang cocok
Enum.any?(list, fn)[a, b, c] → true/falseAda yang cocok?
Enum.all?(list, fn)[a, b, c] → true/falseSemua cocok?
Enum.empty?(list)[] → true, [x] → falseList kosong?
Enum.count(list)[a, b, c] → 3Jumlah elemen
Enum.count(list, fn)[a, b, c] → N yang trueJumlah yang memenuhi
Enum.sum(list)[1, 2, 3] → 6Total (angka)
Enum.sort(list)[3, 1, 2] → [1, 2, 3]Urutkan
Enum.sort_by(list, fn)Sort berdasarkan fieldSort by inserted_at
Enum.uniq(list)[1, 1, 2] → [1, 2]Hilangkan duplikat
Enum.uniq_by(list, fn)Unique berdasarkan fieldUnique by SKU
Enum.group_by(list, fn)list → %{key => [items]}Group orders by status
Enum.flat_map(list, fn)map + flattenOrder items dari banyak orders
Enum.chunk_every(list, n)[a,b,c,d] → [[a,b],[c,d]]Batch processing 50 per batch
Enum.zip(a, b)[1,2] + [:a,:b] → [{1,:a},{2,:b}]Gabungkan 2 list

Checklist Review — Pertanyaan Bab 3

PertanyaanJawaban yang benarRed flag
"Function ini def atau defp?" def = public API, defp = internal helper Logic bisnis penting pakai defp ← gak bisa dipanggil/ditest dari luar
"Kenapa ada \\ di parameter?" "Default value. opts \\ [] = filter opsional." Parameter wajib diberi default ← bisa bikin bug kalau harusnya required
"Siapa yang panggil function ini?" "LiveView panggil context, context panggil context lain atau Repo" LiveView langsung panggil Repo ← melanggar separation of concerns
"Module ini butuh import/alias apa aja?" "alias schema, import Ecto.Query, alias Repo" Import semua (import Ecto.Query, warn: false) ← lazy, bisa masking bugs
"Ada @moduledoc?" "Ya, semua module wajib punya @moduledoc" @moduledoc false atau gak ada sama sekali ← hutang dokumentasi
"Nama function jelas?" "Ya, prefix menjelaskan aksi: create_, list_, process_, ship_" Nama generik: handle, do_stuff, run ← gak bisa ditebak isinya
"Pipe-nya kebaca?" "Ya, bisa dinarasikan dari atas ke bawah" Pipe 15+ langkah ← terlalu panjang, pecah jadi helper function
"Punya test?" "Setiap function def harus punya minimal 1 test" Function tanpa test ← utang teknis

Red Flags — Yang Harus Kamu Tanyakan

1. Logic bisnis di LiveView
Kalau kamu lihat Repo.insert, Repo.update, atau kalkulasi bisnis di file *_live.ex — itu salah. Logic HARUS di context module. LiveView cuma panggil function dari context.
2. Function def tanpa @doc
Setiap function public (def) harusnya punya @doc yang jelaskan: apa yang dilakukan, parameter apa, return apa. Tanpa ini, orang (dan AI) lain harus tebak-tebakan.
3. Circular dependency antar module
Orders panggil Inventory = OK (satu arah).
Inventory panggil balik Orders = RED FLAG (circular).
Dependency harus 1 arah. Kalau 2 arah, arsitektur perlu dipikir ulang.
4. Enum.filter di Elixir padahal bisa WHERE di database
Repo.all(Order) |> Enum.filter(&(&1.status == "dikirim")) ← load SEMUA order, filter di Elixir.
Order |> where([o], o.status == "dikirim") |> Repo.all() ← filter di database, jauh lebih cepat.

Kalau filter bisa dilakukan di query (WHERE), JANGAN dilakukan di Enum.
5. Pipe terlalu panjang (lebih dari 10 step)
Pipe yang panjang = susah di-debug. Pecah jadi function helper kecil dengan nama yang jelas.

Ringkasan Bab 3

Yang kamu sekarang bisa:

1. Baca module structuredefmodule = departemen, 1 file = 1 module
2. Bedakan def vs defp — public (API) vs private (internal)
3. Tebak fungsi dari namanyacreate_, get_!, list_, process_, ship_, void_
4. Baca pipe |> — assembly line, atas ke bawah
5. Baca anonymous functionfn x -> ... end dan shorthand &(&1.field)
6. Kenali Enum functions — map, filter, reduce, find, each, any?
7. Baca guardwhen qty > 0 = syarat tambahan
8. Baca default argsopts \\ [] = opsional
9. Bedakan alias/import/use — shortcut, toolbox, template
10. Cek @moduledoc — dokumentasi wajib ada
11. Kenali @attribute — konstanta compile-time