case, cond, dan with

Alur kendali yang digerakkan oleh pencocokan pola.

Daftar isi

Alur kendali pada Elixir bertumpu pada pencocokan pola: case mencocokkan satu nilai terhadap serangkaian pola, cond mengevaluasi serangkaian syarat boolean, dan with merangkai pencocokan bertingkat untuk alur berhasil-gagal. Topik ini membahas ketiganya beserta error yang muncul ketika tidak ada klausanya yang cocok, lalu menutup dengan contoh utuh validasi nilai ujian. Setelah topik ini selesai, percabangan dapat ditulis dengan gaya pencocokan pola alih-alih tumpukan if yang bersarang.

case: satu nilai, banyak pola#

case mengevaluasi sebuah ekspresi lalu mencocokkan hasilnya dengan klausanya dari atas ke bawah. Klausa pertama yang cocok menentukan hasilnya, dan pencocokan berhenti di situ. Pola pada setiap klausa boleh bertingkat, misalnya tuple bertag yang membawa map di dalamnya, dan setiap klausa dapat diberi guard, yaitu syarat tambahan yang ditulis setelah kata when.

elixir
pesan = {:ok, %{status: 200, isi: "semua baik"}}
hasil = case pesan do
  {:ok, %{status: 200, isi: isi}} -> "berhasil: #{isi}"
  {:ok, %{status: kode}} -> "status tak terduga: #{kode}"
  {:error, alasan} -> "gagal: #{alasan}"
end
IO.puts(hasil)

IO.puts(case {:error, :timeout} do
  {:ok, %{status: 200, isi: isi}} -> "berhasil: #{isi}"
  {:ok, %{status: kode}} -> "status tak terduga: #{kode}"
  {:error, alasan} -> "gagal: #{alasan}"
end)
text Keluaran
berhasil: semua baik
gagal: timeout

Guard mempersempit klausa dengan syarat yang bukan pola, misalnya rentang angka. Klausa yang syaratnya tidak terpenuhi dilewati meskipun polanya cocok, sehingga urutan klausa dari yang paling khusus ke yang paling umum tetap berlaku.

elixir
klasifikasi = fn
  nilai when nilai >= 85 -> "A (#{nilai})"
  nilai when nilai >= 70 -> "B (#{nilai})"
  nilai -> "C (#{nilai})"
end
IO.puts(klasifikasi.(92))
IO.puts(klasifikasi.(71))
IO.puts(klasifikasi.(60))
text Keluaran
A (92)
B (71)
C (60)

case adalah ekspresi, sehingga hasilnya dapat diikat ke variabel seperti pada contoh pertama, dan seluruh badan klausanya dapat memuat beberapa ekspresi sekaligus. Klausa dievaluasi berurutan, dan klausa yang cocok lebih dahulu menang meskipun klausa lain juga cocok. Pola map parsial juga berlaku di sini, termasuk %{} yang cocok dengan map apa pun, sehingga klausa penangkap berbasis pola map wajib diletakkan paling akhir.

Apabila tidak ada klausa yang cocok, CaseClauseError dilempar pada saat program berjalan. Error ini tidak ditangkap saat kompilasi, karena nilai yang dicocokkan memang baru ada saat eksekusi. Klausa penangkap _ -> mencegahnya dengan menampung segala nilai yang tidak terduga.

elixir
try do
  case 3 do
    1 -> "satu"
    2 -> "dua"
  end
rescue
  e in CaseClauseError -> IO.puts("CaseClauseError: #{Exception.message(e)}")
end
text Keluaran
CaseClauseError: no case clause matching:

    3

cond: syarat boolean berurutan#

cond dimaksudkan untuk syarat yang bukan pola, misalnya perbandingan rentang. Setiap syarat dievaluasi dari atas ke bawah, dan badan syarat pertama yang bernilai benar digunakan sebagai hasilnya. Nilai benar (truthy) berarti segala nilai selain false dan nil. Klausa true -> yang diletakkan terakhir berperan sebagai penangkap untuk segala keadaan yang belum tercakup.

elixir
nilai = 78
predikat = cond do
  nilai >= 85 -> "sangat baik"
  nilai >= 70 -> "baik"
  nilai >= 55 -> "cukup"
  true -> "kurang"
end
IO.puts(predikat)
text Keluaran
baik

Urutan syarat pada cond bekerja seperti urutan klausa pada case, dan syarat yang lebih umum harus diletakkan setelah yang lebih khusus. Syarat nilai >= 70 yang diletakkan lebih dahulu akan cocok dengan nilai 90 sebelum syarat nilai >= 85 sempat diuji. Susunan dari yang paling sempit ke paling luas menjaga setiap rentang tetap bermakna.

Apabila seluruh syarat bernilai salah dan tidak ada klausa true ->, CondClauseError dilempar. Perilaku ini mendorong penulisan cond yang menutup semua kemungkinan sejak awal, alih-alih menunggu masukan yang tak terduga untuk memunculkannya.

elixir
try do
  cond do
    1 > 2 -> "mustahil"
  end
rescue
  e in CondClauseError -> IO.puts("CondClauseError: #{Exception.message(e)}")
end
text Keluaran
CondClauseError: no cond clause evaluated to a truthy value

with: rangkaian pencocokan#

with menyambung beberapa tahap, dan setiap tahap menghasilkan nilai seperti {:ok, _} atau {:error, _}. Tanda panah <- mencocokkan hasil setiap tahap dengan pola di kirinya. Apabila cocok, eksekusi berlanjut ke tahap berikutnya dan variabel pada polanya terikat. Apabila seluruh tahap cocok, badan do dieksekusi, sedangkan nilai yang gagal dialihkan ke blok else.

elixir
defmodule Alur do
  def jalankan(nilai) do
    with {:ok, angka} <- to_angka(nilai),
         {:ok, ganda} <- gandakan(angka) do
      "hasil: #{ganda}"
    else
      {:error, alasan} -> "ditolak: #{alasan}"
    end
  end

  defp to_angka(nilai) when is_integer(nilai), do: {:ok, nilai}
  defp to_angka(_), do: {:error, "bukan bilangan bulat"}

  defp gandakan(angka) when angka > 0, do: {:ok, angka * 2}
  defp gandakan(_), do: {:error, "harus positif"}
end

IO.puts(Alur.jalankan(21))
IO.puts(Alur.jalankan("teks"))
IO.puts(Alur.jalankan(-5))
text Keluaran
hasil: 42
ditolak: bukan bilangan bulat
ditolak: harus positif

Keluaran di atas menunjukkan tiga jalur yang berbeda, yaitu berhasil sampai akhir, gagal pada tahap pertama, dan gagal pada tahap kedua. Blok else menangkap kegagalan dari tahap mana pun tanpa struktur bersarang. Apabila else tidak disediakan, nilai yang gagal dikembalikan apa adanya, sehingga kegagalan mengalir diam-diam ke pemanggil.

elixir
hasil = with {:ok, x} <- :tidak_ok do
  x
end
IO.inspect(hasil, label: "nilai with tanpa else")
text Keluaran
nilai with tanpa else: :tidak_ok

Blok else sendiri tunduk pada aturan pencocokan yang sama dengan case. Apabila nilai yang gagal tidak cocok dengan klausa else mana pun, WithClauseError dilempar dan eksekusi berhenti. Blok else yang lengkap menutup setiap bentuk kegagalan yang mungkin dihasilkan tahapan, misalnya klausa penangkap {:error, alasan} pada contoh utuh di bawah, yang mencakup {:error, alasan} dari tahap mana pun.

Contoh utuh: berkas proses_nilai.exs#

Berkas berikut merangkai pola with pada alur validasi nilai ujian. Tahap pertama memeriksa keberadaan kunci :nilai dan tipenya, tahap kedua menetapkan rentang yang sah beserta predikatnya, dan blok else menangkap {:error, alasan} dari tahap mana pun. Jalankan berkasnya dengan perintah elixir proses_nilai.exs dari direktori topik.

elixir proses_nilai.exs
# proses_nilai.exs - alur validasi nilai ujian dengan with
defmodule ProsesNilai do
  def proses(input) do
    with {:ok, nilai} <- ambil_nilai(input),
         {:ok, predikat} <- klasifikasi(nilai) do
      "Nilai #{nilai}: #{predikat}"
    else
      {:error, alasan} -> "Ditolak: #{alasan}"
    end
  end

  defp ambil_nilai(input) do
    case Map.fetch(input, :nilai) do
      {:ok, nilai} when is_integer(nilai) -> {:ok, nilai}
      {:ok, _} -> {:error, "nilai bukan bilangan bulat"}
      :error -> {:error, "kunci :nilai tidak ditemukan"}
    end
  end

  defp klasifikasi(nilai) when nilai < 0 or nilai > 100, do: {:error, "di luar rentang 0-100"}
  defp klasifikasi(nilai) when nilai >= 70, do: {:ok, "lulus"}
  defp klasifikasi(nilai) when nilai >= 55, do: {:ok, "lulus bersyarat"}
  defp klasifikasi(_nilai), do: {:ok, "tidak lulus"}
end

IO.puts(ProsesNilai.proses(%{nilai: 85}))
IO.puts(ProsesNilai.proses(%{nilai: 60}))
IO.puts(ProsesNilai.proses(%{nilai: 40}))
IO.puts(ProsesNilai.proses(%{nilai: 150}))
IO.puts(ProsesNilai.proses(%{nama: "Budi"}))
text Keluaran
Nilai 85: lulus
Nilai 60: lulus bersyarat
Nilai 40: tidak lulus
Ditolak: di luar rentang 0-100
Ditolak: kunci :nilai tidak ditemukan

Kelima baris keluaran mewakili lima jalur yang berbeda. Tiga jalur pertama mengalir sampai badan do, sedangkan dua jalur terakhir berhenti pada tahap yang menolaknya. Fungsi ambil_nilai mengembalikan {:error, alasan} untuk dua keadaan, yaitu kunci yang hilang dan tipe yang keliru, sehingga pesan penolakannya spesifik. Fungsi klasifikasi menggunakan guard untuk rentang dan predikat, dan klausa terakhirnya menampung nilai 0 sampai 54 tanpa menyebut batasnya secara eksplisit. Tanpa with, rantai ini biasanya ditulis sebagai case yang bersarang dua tingkat, dan setiap tambahan tahap memperdalam sarangnya.

Kesalahan umum#

  • Menulis case tanpa klausa penangkap. Nilai di luar dugaan melempar CaseClauseError saat eksekusi. Perbaikannya, tambahkan _ -> sebagai klausa terakhir yang menyatakan perilaku bawaan secara eksplisit.
  • Menggunakan with tanpa else. Nilai yang gagal dikembalikan apa adanya dan kegagalan mengalir diam-diam. Perbaikannya, sediakan else yang polanya menutup semua bentuk kegagalan tahapan.
  • Menyediakan else yang klausanya tidak menutup semua bentuk kegagalan. Nilai yang lolos melempar WithClauseError, seperti pada contoh berikut.
elixir
try do
  hasil = with {:ok, x} <- :tidak_ok do
    x
  else
    :lain -> "lain"
  end
  IO.inspect(hasil, label: "hasil")
rescue
  e in WithClauseError -> IO.puts("WithClauseError: #{Exception.message(e)}")
end
text Keluaran
WithClauseError: no with clause matching:

    :tidak_ok
  • Menulis cond tanpa klausa true -> di akhir. Masukan di luar semua syarat melempar CondClauseError. Perbaikannya, akhiri cond dengan true -> yang menyatakan kasus sisanya.

Ringkasan#

  • case mencocokkan satu nilai dengan pola bertingkat yang dapat dipersempit dengan guard, dan melempar CaseClauseError apabila tidak ada klausanya yang cocok.
  • cond mengevaluasi syarat boolean dari atas ke bawah dan menuntut true -> sebagai penangkap terakhir.
  • with merangkai pencocokan bertingkat dengan <-, mengeksekusi badan do pada jalur berhasil, dan mengalihkan kegagalan ke else.

Referensi#