Skip to main content

A.39. Tipe Data ➜ Result

Chapter ini membahas tentang tipe data Result. Tipe data ini digunakan untuk menampung nilai hasil suatu proses yang isinya adalah bisa sukses (Ok) atau error (Err).

Tipe data Result biasa digunakan untuk menampung hasil eksekusi proses dan error handling.

A.39.1. Konsep Result

Tipe data Result adalah enum dengan isi 2 buah enum value:

  • Result::Ok<T> (atau Ok<T>), digunakan untuk menandai bahwa data isinya adalah kabar baik (oke / mantab / jos / sukses).
  • Result::Err<E> (atau Err<E>), digunakan untuk menandai bawah data berisi kabar buruk.
  • T dan E merupakan parameter generic. Lebih jelasnya mengenai generic dibahas pada chapter Generics.

Tipe data Result memiliki notasi penulisan Result<T, E> dimana T digunakan pada enum value Ok<T> dan E digunakan enum value Err<E>.

Cara penerapan tipe data ini bisa dilihat pada kode berikut:

#[derive(Debug)]
enum MathError {
DivisionByZero,
InfinityNumber,
OtherError,
}

fn main() {
let result1 = divider(10.0, 5.0);
println!("result: {:?}", result1);

let result2: Result<f64, MathError> = divider(10.0, 0.0);
println!("result: {:?}", result2);
}

fn divider(a: f64, b: f64) -> Result<f64, MathError> {
if b == 0.0 {
return Err(MathError::DivisionByZero);
}

let result = a / b;
return Ok(result);
}

Fungsi divider di atas tugasnya adalah melakukan operasi aritmatika pembagian angka numerik f64, parameter a dibagi b.

Pada fungsi tersebut terdapat pengecekan apabila nilai b adalah 0, maka yang dikembalikan adalah Err<E> dengan E berisi pesan error, selainnya maka hasil operasi pembagian dikembalikan dibungkus dalam enum value Ok<f64>.

Fungsi divider nilai baliknya bertipe Result<f64, MathError>. Dari tipe data yang digunakan nantinya bisa diprediksi pasti akan ada 2 potensi value:

  • Return value adalah enum value Err<MathError>, muncul ketika nilai b adalah 0
  • Return value adalah nilai hasil numerik yang dibungkus oleh enum value Ok<f64>

Output program di atas saat di-run:

Result type

A.39.2. Pattern matching pada tipe Result

Dalam penerapannya, ketika ada data bertipe Result artinya data tersebut berpotensi untuk berisi nilai Err<E> atau Ok<T>, pasti antara 2 nilai tersebut.

Umumnya penggunaan tipe Result selalu diikuti dengan pattern matching menggunakan keyword match. Selain itu keyword if sebenarnya juga bisa diterapkan pada pattern matching tipe data ini, namun kurang dianjurkan.

Mari kita praktikkan. Ubah isi fungsi main dengan kode berikut:

let result = divider(10.0, 5.0);
match result {
Err(m) => println!("ERROR! {:?}", m),
Ok(r) => println!("result: {r:.2}"),
}

Option type

Bisa dilihat pada kode di atas mudahnya pengambilan nilai m dari Err(m) dan juga r dari Ok(r). Penerapan match untuk seleksi kondisi biasa disebut dengan pattern matching dan teknik ini sangat fleksibel dan advance.

Sebagai contoh, dengan penerapan match yang seperti ini kita bisa meng-handle 5 skenario seleksi kondisi:

let result = divider(10.0, 5.0);
match result {
Err(MathError::DivisionByZero) => println!("ERROR! unable to divide number by 0"),
Err(MathError::InfinityNumber) => println!("ERROR! result is infinity number (∞)"),
Err(_) => println!("ERROR! unknown error"),
Ok(2.0) => println!("the result is 2"),
Ok(x) => println!("result: {x:.2}"),
}
  • Kondisi ke-1: jika nilai adalah Err(MathError::DivisionByZero), maka munculkan pesan ERROR! unable to divide number by 0.
  • Kondisi ke-2: jika nilai adalah Err(MathError::InfinityNumber), maka munculkan pesan ERROR! result is infinity number (∞).
  • Kondisi ke-3: jika nilai adalah Err selain dari Err(MathError::DivisionByZero) dan Err(MathError::InfinityNumber), maka munculkan pesan ERROR! unknown error.
  • Kondisi ke-4: jika nilai adalah Ok(2.0), maka munculkan pesan the result is 2.
  • Kondisi ke-5: jika nilai adalah Ok selain dari Ok(2.0), maka munculkan pesan result: {x:.2}.

◉ Tips pattern matching

Silakan perhatikan kode yang sudah kita praktikkan berikut ini:

let result = divider(10.0, 5.0);
match result {
Err(m) => println!("ERROR! {:?}", m),
Ok(r) => println!("result: {r:.2}"),
}

Penerapan pattern matching seperti contoh di atas memiliki konsekuensi, yaitu variabel r hanya bisa diakses pada block Ok(r) saja.

Adakalanya kita butuh untuk mengeluarkan variabel r ke luar block. Hal seperti ini mudah untuk dilakukan, dan ada beberapa cara yang bisa dipilih, namun menurut penulis yang paling elegan adalah cara berikut ini:

fn main() {
let result: f64 = match divider(10.0, 5.0) {
Err(m) => {
println!("ERROR! {:?}", m);
0.0
},
Ok(r) => r,
};

println!("result: {:?}", result);
}

Statement divider(10.0, 5.0) mengembalikan data bertipe Result<f64, MathError>. Data tersebut digunakan pada keyword match seperti biasa. Namun pada contoh di atas ada yang berbeda, yaitu return value dari statement match ditampung ke variabel (result).

Isi dari pattern matching match sendiri ada dua:

  • Ketika block Err(m) match, error di-print kemudian nilai 0.0 dijadikan return statement match.
  • Ketika block Ok match, data r dijadikan return value statement match.

Dengan penerapan pattern matching seperti di atas, maka variabel result akan selalu berisi data hasil operasi divider(10.0, 5.0). Dengan pengecualian ketika ada error, pesan errornya dimunculkan kemudian hasil operasi pembagian di-set sebagai 0.0.

Lebih jelasnya mengenai pattern matching dibahas pada chapter Pattern Matching

A.39.3. Method tipe data Result

◉ Method is_ok & unwrap

Isi dari enum value Ok<T> bisa diakses tanpa menggunakan keyword match dengan cara memanfaatkan method unwrap milik Result<T, E>. Sebelum mengakses method tersebut sangat dianjurkan untuk mengecek apakah data berisi Ok<T> atau tidak, karena jika data adalah Err<E> pengaksesan method unwrap menghasilkan error.

Pengecekan nilai ok atau tidak bisa dilakukan menggunakan method is_ok.

let result = divider(10.0, 5.0);
if result.is_ok() {
let number = result.unwrap();
println!("result: {}", number);
// result: 2
}

Result type

◉ Method as_ref

Method as_ref digunakan untuk mengakses reference T dan E pada Result<T, E>. Method ini sering kali dibutuhkan untuk menghindari terjadinya move semantics pada owner data bertipe Result<T, E>.

Method as_ref mengembalikan data dalam tipe Result<&T, &E>. Jadi reference yang dipinjam bukan milik Result-nya melainkan milik T dan E.

let result: Result<f64, MathError> = divider(10.0, 0.0);
let result_borrow: Result<&f64, &MathError> = result.as_ref();

Lebih jelasnya mengenai move semantics dibahas pada chapter Ownership

◉ Method is_err & err

Method err mengembalikan data dalam tipe Err<E>. Pada pengaksesan method ini, pastikan untuk mengecek apakah Result berisi data error atau ok dengan via method is_err. Selain itu, wajib untuk menggunakan method as_ref sebelum method err agar ownership data Result tidak berpindah (move semantics).

let result = divider(10.0, 0.0);
if result.is_err() {
let err = result.as_ref().err();
let message = err.unwrap();
println!("error: {:?}", message);
// error: DivisionByZero
}

◉ Method ok

Aturan yang sama juga berlaku pada pengaksesan method ok yang mengembalikan data Ok<T>. Method as_ref harus diakses terlebih dahulu sebelum memanggil method ok agar tidak terjadi move semantics.

let result = divider(10.0, 5.0);
if result.is_ok() {
let data = result.as_ref().ok();
let number = data.unwrap();
println!("result: {:?}", number);
// result: 2
}

◉ Method unwrap_or_default

Method unwrap_or_default milik Result<T, E> mengembalikan nilai T ketika data berisi Ok<T>, namun jika data berisi Err<E> maka yang dikembalikan adalah default value dari tipe data T.

let result = divider(10.0, 0.0);
let number = result.unwrap_or_default();
println!("result: {}", number);
// result: 0

◉ Method unwrap_or

Method unwrap_or milik Result<T, E> mengembalikan nilai T ketika data berisi Ok<T>, namun jika data ternyata isinya adalah Err<E> maka yang dikembalikan adalah argument pemanggilan method tersebut.

let result = divider(10.0, 0.0);
let number = result.unwrap_or(0.0);
println!("result: {}", number);
// result: 0

◉ Method unwrap_or_else

Method ini mengembalikan nilai T ketika data berisi Ok<T>, namun jika data isinya adalah Err<E> maka yang dikembalikan adalah hasil eksekusi closure yang disisipkan saat memanggil method unwrap_or_else. Contoh pengaplikasiannya:

let result = divider(10.0, 0.0);
let number = result.unwrap_or_else(|_| 0.0);
println!("result: {}", number);
// result: 0

Closure harus dalam notasi FnOnce(E) -> T dimana T pada konteks ini adalah f64.

Lebih jelasnya mengenai closure dibahas pada chapter Closures.

A.39.4. Error handling tipe Result

Tipe data Result<T, E> banyak digunakan pada fungsi milik Rust standard library, dan kita selaku programmer pastinya juga akan menggunakannya dalam real life project.

Tipe ini dipakai salah satunya untuk manajemen error. Lebih jelasnya mengenai topik tersebut dibahas pada chapter Error Handling & Panic


Catatan chapter 📑

◉ Source code praktik

github.com/novalagung/dasarpemrogramanrust-example/../result_type

◉ Chapter relevan lainnya

◉ Work in progress

  • Operator ?

◉ Referensi