Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Closures: Fungsi Anonim yang Bisa Menangkap Lingkungannya

Closures di Rust adalah fungsi anonim (tanpa nama) yang bisa kita simpan di dalam sebuah variabel atau diteruskan sebagai argumen ke fungsi lain. Kita bisa membuat sebuah closure di satu tempat lalu memanggil closure tersebut di tempat lain untuk dievaluasi dalam konteks yang berbeda. Tidak seperti fungsi biasa, closures bisa menangkap (capture) nilai-nilai dari scope tempat mereka didefinisikan. Kita bakal mendemonstrasikan gimana fitur-fitur closure ini memungkinkan penggunaan ulang kode dan kustomisasi perilaku.

Menangkap Lingkungan Menggunakan Closures

Pertama-tama kita bakal meneliti gimana kita bisa memakai closures buat menangkap nilai dari lingkungan tempat mereka didefinisikan untuk dipakai nanti. Berikut skenarionya: sesekali, perusahaan kaos kita membagikan kaos edisi terbatas eksklusif kepada seseorang di mailing list kita sebagai bentuk promosi. Orang-orang di mailing list bisa secara opsional menambahkan warna favorit mereka ke profilnya. Kalau orang yang terpilih untuk dapat kaos gratis itu sudah menge-set warna favoritnya, dia bakal dapat kaos dengan warna itu. Tapi kalau orang tersebut belum menentukan warna favorit, dia bakal dapat warna apa pun yang saat itu stoknya paling banyak di perusahaan.

Ada banyak cara buat mengimplementasikan ini. Buat contoh ini, kita bakal memakai sebuah enum bernama ShirtColor yang punya varian Red (merah) dan Blue (biru) (kita membatasi jumlah warna yang ada biar simpel). Kita mewakili stok barang milik perusahaan dengan sebuah struct Inventory yang punya field bernama shirts yang berisi sebuah Vec<ShirtColor> yang merepresentasikan warna-warna kaos yang saat ini ada di stok. Method giveaway yang didefinisikan pada Inventory menerima preferensi warna kaos opsional dari pemenang kaos gratis, lalu mengembalikan warna kaos yang bakal didapat oleh orang tersebut. Persiapan ini ditunjukkan di Listing 13-1.

Filename: src/main.rs
#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let mut num_red = 0;
        let mut num_blue = 0;

        for color in &self.shirts {
            match color {
                ShirtColor::Red => num_red += 1,
                ShirtColor::Blue => num_blue += 1,
            }
        }
        if num_red > num_blue {
            ShirtColor::Red
        } else {
            ShirtColor::Blue
        }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let user_pref1 = Some(ShirtColor::Red);
    let giveaway1 = store.giveaway(user_pref1);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref1, giveaway1
    );

    let user_pref2 = None;
    let giveaway2 = store.giveaway(user_pref2);
    println!(
        "The user with preference {:?} gets {:?}",
        user_pref2, giveaway2
    );
}
Listing 13-1: Skenario pembagian hadiah dari perusahaan kaos

Toko (store) yang didefinisikan di fungsi main punya dua kaos biru dan satu kaos merah yang tersisa untuk dibagikan dalam promosi edisi terbatas ini. Kita memanggil method giveaway untuk seorang user dengan preferensi kaos merah dan seorang user tanpa preferensi sama sekali.

Sekali lagi, kode ini bisa saja diimplementasikan dengan banyak cara, dan di sini, untuk fokus ke closures, kita cuma memakai konsep-konsep yang sudah kita pelajari, kecuali buat body dari method giveaway yang memakai sebuah closure. Di method giveaway, kita menerima preferensi user sebagai sebuah parameter bertipe Option<ShirtColor> lalu memanggil method unwrap_or_else pada user_preference. Method unwrap_or_else pada Option<T> didefinisikan oleh standard library. Method ini menerima satu argumen: sebuah closure tanpa argumen apa pun yang mengembalikan sebuah nilai bertipe T (tipe yang sama dengan yang disimpan di varian Some dari Option<T>, yang mana di kasus ini adalah ShirtColor). Kalau Option<T> itu adalah varian Some, unwrap_or_else bakal mengembalikan nilai dari dalam Some tersebut. Tapi kalau Option<T> adalah varian None, unwrap_or_else bakal memanggil closure-nya dan mengembalikan nilai yang dikembalikan oleh closure tersebut.

Kita menentukan ekspresi closure || self.most_stocked() sebagai argumen untuk unwrap_or_else. Ini adalah sebuah closure yang tidak menerima parameter apa pun (kalau closure-nya punya parameter, parameternya bakal muncul di antara dua garis vertikal). Body dari closure ini memanggil self.most_stocked(). Kita mendefinisikan closure-nya di sini, dan implementasi dari unwrap_or_else nanti bakal mengevaluasi closure ini kalau hasilnya memang dibutuhkan.

Menjalankan kode ini bakal mencetak output berikut:

$ cargo run
   Compiling shirt-company v0.1.0 (file:///projects/shirt-company)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/shirt-company`
The user with preference Some(Red) gets Red
The user with preference None gets Blue

Satu aspek yang menarik di sini adalah kita sudah meneruskan sebuah closure yang memanggil self.most_stocked() pada instance Inventory saat ini. Standard library tidak perlu tahu apa-apa tentang tipe Inventory atau ShirtColor yang kita definisikan, ataupun logika yang mau kita pakai di skenario ini. Closure ini menangkap sebuah referensi immutable ke instance Inventory self dan meneruskannya bersama kode yang kita tentukan ke method unwrap_or_else. Di sisi lain, fungsi biasa tidak bisa menangkap lingkungan mereka dengan cara seperti ini.

Inference dan Anotasi Tipe untuk Closure

Ada lebih banyak perbedaan antara fungsi biasa dan closures. Closures biasanya tidak mengharuskan kita untuk menganotasi tipe dari parameter atau nilai kembalian seperti yang diwajibkan oleh fungsi fn. Anotasi tipe diwajibkan pada fungsi karena tipe-tipe tersebut adalah bagian dari antarmuka eksplisit yang diekspos ke para pengguna fungsi tersebut. Mendefinisikan antarmuka ini secara kaku penting untuk memastikan bahwa semua orang sepakat mengenai tipe-tipe nilai yang dipakai dan dikembalikan oleh sebuah fungsi. Sebaliknya, closures tidak dipakai dalam antarmuka yang terekspos seperti itu: mereka disimpan di dalam variabel dan dipakai tanpa menamai mereka atau mengeksposnya ke pengguna library kita.

Closures biasanya berukuran pendek dan hanya relevan di dalam konteks yang sempit, bukan di sembarang skenario acak. Di dalam batasan konteks ini, compiler bisa menebak (infer) tipe-tipe parameternya dan tipe kembaliannya, mirip dengan bagaimana ia bisa menebak tipe dari sebagian besar variabel (walaupun ada kasus-kasus langka di mana compiler juga membutuhkan anotasi tipe untuk closure).

Sama halnya dengan variabel, kita bisa menambahkan anotasi tipe kalau kita mau meningkatkan kejelasan secara eksplisit walau akibatnya kode kita bakal sedikit lebih bertele-tele (verbose) dari yang sebenarnya diperlukan. Menganotasi tipe buat sebuah closure bakal kelihatan seperti definisi yang ditunjukkan di Listing 13-2. Di contoh ini, kita mendefinisikan sebuah closure dan menyimpannya di dalam sebuah variabel, bukannya mendefinisikan closure tersebut tepat di tempat kita meneruskannya sebagai argumen seperti yang kita lakukan di Listing 13-1.

Filename: src/main.rs
use std::thread;
use std::time::Duration;

fn generate_workout(intensity: u32, random_number: u32) {
    let expensive_closure = |num: u32| -> u32 {
        println!("calculating slowly...");
        thread::sleep(Duration::from_secs(2));
        num
    };

    if intensity < 25 {
        println!("Today, do {} pushups!", expensive_closure(intensity));
        println!("Next, do {} situps!", expensive_closure(intensity));
    } else {
        if random_number == 3 {
            println!("Take a break today! Remember to stay hydrated!");
        } else {
            println!(
                "Today, run for {} minutes!",
                expensive_closure(intensity)
            );
        }
    }
}

fn main() {
    let simulated_user_specified_value = 10;
    let simulated_random_number = 7;

    generate_workout(simulated_user_specified_value, simulated_random_number);
}
Listing 13-2: Menambahkan anotasi tipe opsional buat tipe parameter dan nilai kembalian di closure

Dengan menambahkan anotasi tipe, sintaks closures jadi kelihatan lebih mirip dengan sintaks fungsi biasa. Di sini, kita mendefinisikan sebuah fungsi yang menambahkan 1 ke parameternya dan sebuah closure yang punya perilaku yang sama, sebagai perbandingan. Kita sudah menambahkan sedikit spasi supaya bagian-bagian yang relevan sejajar. Ini mengilustrasikan gimana sintaks closure itu mirip dengan sintaks fungsi, kecuali di penggunaan garis vertikal (|) dan seberapa banyak sintaks yang sifatnya opsional:

fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

Baris pertama menunjukkan sebuah definisi fungsi dan baris kedua menunjukkan definisi closure yang dianotasi secara penuh. Di baris ketiga, kita membuang anotasi tipe dari definisi closure. Di baris keempat, kita membuang kurung kurawal, yang mana jadi opsional karena body dari closure ini hanya punya satu ekspresi. Ini semua adalah definisi yang valid dan bakal menghasilkan perilaku yang sama saat mereka dipanggil. Baris add_one_v3 dan add_one_v4 mewajibkan closures tersebut untuk dievaluasi agar kodenya bisa di-compile, karena tipe-tipenya bakal ditebak berdasarkan gimana closures tersebut dipakai. Ini mirip dengan bagaimana let v = Vec::new(); membutuhkan entah anotasi tipe atau adanya nilai dengan tipe tertentu yang dimasukkan ke dalam Vec agar Rust bisa menebak tipenya.

Buat definisi closure, compiler bakal menebak satu tipe konkret untuk masing- masing parameternya dan juga untuk nilai kembaliannya. Misalnya, Listing 13-3 menunjukkan definisi closure singkat yang cuma mengembalikan nilai yang dia terima sebagai parameter. Closure ini sebenarnya tidak terlalu berguna kecuali buat tujuan contoh ini. Perhatikan bahwa kita tidak menambahkan anotasi tipe apa pun di definisinya. Karena tidak ada anotasi tipe, kita bisa memanggil closure ini dengan tipe apa pun, yang mana kita lakukan di sini dengan tipe String untuk panggilan pertama. Kalau kita kemudian mencoba memanggil example_closure dengan sebuah integer, kita bakal dapat error.

Filename: src/main.rs
fn main() {
    let example_closure = |x| x;

    let s = example_closure(String::from("hello"));
    let n = example_closure(5);
}
Listing 13-3: Mencoba memanggil sebuah closure yang tipenya masih ditebak memakai dua tipe yang berbeda

Compiler ngasih kita error ini:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
error[E0308]: mismatched types
 --> src/main.rs:5:29
  |
5 |     let n = example_closure(5);
  |             --------------- ^ expected `String`, found integer
  |             |
  |             arguments to this function are incorrect
  |
note: expected because the closure was earlier called with an argument of type `String`
 --> src/main.rs:4:29
  |
4 |     let s = example_closure(String::from("hello"));
  |             --------------- ^^^^^^^^^^^^^^^^^^^^^ expected because this argument is of type `String`
  |             |
  |             in this closure call
note: closure parameter defined here
 --> src/main.rs:2:28
  |
2 |     let example_closure = |x| x;
  |                            ^
help: try using a conversion method
  |
5 |     let n = example_closure(5.to_string());
  |                              ++++++++++++

For more information about this error, try `rustc --explain E0308`.
error: could not compile `closure-example` (bin "closure-example") due to 1 previous error

Saat pertama kali kita memanggil example_closure memakai nilai String, compiler menebak kalau tipe dari x dan tipe kembalian dari closure itu adalah String. Tipe-tipe itu kemudian “terkunci” ke dalam closure di example_closure, dan kita bakal dapat type error (error tipe) saat kita mencoba memakai tipe yang berbeda dengan closure yang sama.

Menangkap Referensi atau Memindahkan Kepemilikan (Ownership)

Closures bisa menangkap nilai dari lingkungannya memakai tiga cara, yang mana berkorelasi langsung dengan tiga cara sebuah fungsi bisa menerima parameter: meminjam secara immutable, meminjam secara mutable, dan mengambil kepemilikan (taking ownership). Closure bakal memutuskan cara mana yang mau dipakai berdasarkan apa yang dilakukan oleh isi fungsinya terhadap nilai-nilai yang ditangkapnya.

Di Listing 13-4, kita mendefinisikan sebuah closure yang menangkap referensi immutable ke vector bernama list karena closure itu cuma butuh referensi immutable buat mencetak nilainya.

Filename: src/main.rs
fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let only_borrows = || println!("From closure: {list:?}");

    println!("Before calling closure: {list:?}");
    only_borrows();
    println!("After calling closure: {list:?}");
}
Listing 13-4: Mendefinisikan dan memanggil sebuah closure yang menangkap sebuah referensi immutable

Contoh ini juga mengilustrasikan kalau sebuah variabel bisa diikat ke definisi sebuah closure, dan nanti kita bisa memanggil closure tersebut memakai nama variabel dan tanda kurung, seolah-olah nama variabel itu adalah nama sebuah fungsi.

Karena kita bisa punya banyak referensi immutable ke list di waktu yang bersamaan, list masih bisa diakses dari kode sebelum definisi closure, di antara definisi closure namun sebelum closure-nya dipanggil, dan setelah closure-nya dipanggil. Kode ini berhasil di-compile, jalan, dan mencetak:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
Before calling closure: [1, 2, 3]
From closure: [1, 2, 3]
After calling closure: [1, 2, 3]

Selanjutnya, di Listing 13-5, kita ngubah body closure-nya biar dia nambahin sebuah elemen ke vector list. Closure ini sekarang menangkap referensi mutable.

Filename: src/main.rs
fn main() {
    let mut list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    let mut borrows_mutably = || list.push(7);

    borrows_mutably();
    println!("After calling closure: {list:?}");
}
Listing 13-5: Mendefinisikan dan memanggil sebuah closure yang menangkap sebuah referensi mutable

Kode ini berhasil di-compile, jalan, dan mencetak:

$ cargo run
   Compiling closure-example v0.1.0 (file:///projects/closure-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/closure-example`
Before defining closure: [1, 2, 3]
After calling closure: [1, 2, 3, 7]

Perhatikan bahwa sekarang tidak ada lagi println! di antara definisi dan pemanggilan closure borrows_mutably: ketika borrows_mutably didefinisikan, ia menangkap referensi mutable ke list. Kita tidak lagi menggunakan closure ini setelah ia dipanggil, jadi peminjaman mutable itu pun berakhir. Di antara definisi closure dan pemanggilannya, kita tidak diizinkan buat melakukan peminjaman immutable untuk mencetaknya karena peminjaman lain tidak diperbolehkan selama masih ada peminjaman mutable. Coba tambahkan println! di situ buat melihat pesan error apa yang bakal kita dapatkan!

Kalau kita mau memaksa closure buat mengambil ownership dari nilai yang dia pakai dari lingkungannya, biarpun isi closure-nya sebenarnya tidak mewajibkan ownership, kita bisa menambahkan keyword move sebelum daftar parameternya.

Teknik ini biasanya sangat berguna pas kita mau meneruskan sebuah closure ke thread baru untuk memindahkan datanya supaya ia dimiliki oleh thread baru tersebut. Kita bakal bahas threads dan kenapa kita mau menggunakannya secara mendetail di Bab 16 saat kita ngomongin soal konkurensi (concurrency), tapi buat sekarang, mari kita eksplor secara singkat cara bikin thread baru yang memakai sebuah closure yang membutuhkan keyword move. Listing 13-6 menampilkan kode dari Listing 13-4 yang diubah buat mencetak isi vector di sebuah thread baru, bukannya di thread utama (main thread).

Filename: src/main.rs
use std::thread;

fn main() {
    let list = vec![1, 2, 3];
    println!("Before defining closure: {list:?}");

    thread::spawn(move || println!("From thread: {list:?}"))
        .join()
        .unwrap();
}
Listing 13-6: Memakai move buat memaksa closure di thread baru untuk mengambil kepemilikan dari list

Kita membuat thread baru, sambil memberikan sebuah closure buat dijalankan sebagai argumen untuk thread tersebut. Isi closure-nya mencetak daftar list. Di Listing 13-4, closure tersebut cuma menangkap list dengan memakai referensi immutable karena itulah hak akses minimal yang dibutuhkan ke list buat mencetaknya. Di contoh ini, meskipun isi closure-nya masih cuma butuh referensi immutable, kita harus menentukan secara spesifik bahwa list seharusnya dipindahkan (moved) ke dalam closure dengan menaruh keyword move di awal definisi closure-nya. Kalau thread utamanya melakukan lebih banyak operasi sebelum memanggil join pada thread barunya, thread baru tersebut bisa saja selesai sebelum thread utamanya selesai, atau sebaliknya thread utama bisa saja selesai lebih dulu. Kalau thread utama masih mempertahankan ownership dari list tapi dia berakhir lebih dulu sebelum thread barunya dan men-drop (membuang) list tersebut, referensi immutable di thread baru bakal jadi tidak valid. Maka dari itu, compiler mewajibkan list untuk dipindahkan ke dalam closure yang diberikan ke thread baru supaya referensinya bakal dipastikan valid. Coba hapus keyword move-nya atau coba pakai variabel list di thread utama setelah closure itu didefinisikan buat melihat error compiler apa yang bakal muncul!

Memindahkan Nilai yang Ditangkap ke Luar Closures dan Traits Fn

Begitu sebuah closure sudah menangkap referensi atau mengambil kepemilikan dari sebuah nilai dari lingkungan tempat closure itu didefinisikan (yang artinya, hal itu memengaruhi apa yang dipindahkan ke dalam closure-nya), kode di dalam isi closure-nya bakal menentukan apa yang terjadi sama referensi atau nilai tersebut pas closure-nya dievaluasi nantinya (yang artinya, hal ini memengaruhi apa yang dipindahkan ke luar dari closure-nya).

Isi dari sebuah closure bisa ngelakuin mana aja dari hal-hal berikut: memindahkan nilai yang ditangkap ke luar dari closure, memutasi (mengubah) nilai yang ditangkap, tidak memindahkan atau memutasi nilainya, atau dari awal emang tidak menangkap apa pun dari lingkungannya.

Cara sebuah closure menangkap dan menangani nilai dari lingkungannya memengaruhi trait mana yang diimplementasikan oleh closure tersebut, dan traits adalah cara bagaimana fungsi dan struct bisa menentukan jenis closures apa yang bisa mereka terima. Closures bakal secara otomatis mengimplementasikan satu, dua, atau ketiga traits Fn ini secara aditif, tergantung dari gimana isi closure-nya menangani nilai-nilai tersebut:

  • FnOnce berlaku untuk closures yang bisa dipanggil sekali saja. Semua closures minimal mengimplementasikan trait ini karena semua closures itu bisa dipanggil. Closure yang memindahkan nilai yang ditangkap keluar dari isi kodenya cuma bakal mengimplementasikan FnOnce dan bukan trait Fn lainnya karena ia cuma bisa dipanggil satu kali.
  • FnMut berlaku untuk closures yang tidak memindahkan nilai yang ditangkap keluar dari isinya, tapi yang mungkin memutasi nilai-nilai tersebut. Closures jenis ini bisa dipanggil lebih dari sekali.
  • Fn berlaku untuk closures yang tidak memindahkan nilai yang ditangkap keluar dari isinya dan tidak memutasi nilai-nilai tersebut, serta berlaku juga buat closures yang memang tidak menangkap apa pun dari lingkungannya. Closures seperti ini bisa dipanggil lebih dari sekali tanpa memutasi lingkungannya, yang mana ini penting buat kasus-kasus seperti saat kita memanggil sebuah closure berkali-kali secara konruen (bersamaan).

Mari kita lihat definisi dari method unwrap_or_else pada Option<T> yang kita pakai di Listing 13-1:

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(x) => x,
            None => f(),
        }
    }
}

Ingat kembali bahwa T adalah tipe generik yang merepresentasikan tipe dari nilai di dalam varian Some milik sebuah Option. Tipe T itu juga adalah tipe kembalian (return type) dari fungsi unwrap_or_else: kode yang memanggil unwrap_or_else pada sebuah Option<String>, misalnya, bakal mendapatkan sebuah String.

Selanjutnya, perhatikan bahwa fungsi unwrap_or_else juga punya parameter tipe generik tambahan F. Tipe F adalah tipe dari parameter bernama f, yang mana itu adalah closure yang kita kasih saat memanggil unwrap_or_else.

Trait bound yang ditentukan pada tipe generik F adalah FnOnce() -> T, yang berarti F harus bisa dipanggil sekali, tidak menerima argumen apa pun, dan mengembalikan sebuah T. Memakai FnOnce di trait bound ini mengekspresikan batasan bahwa unwrap_or_else cuma bakal memanggil f maksimal satu kali saja. Di dalam isi unwrap_or_else, kita bisa melihat kalau Option-nya itu Some, f tidak bakal dipanggil. Kalau Option-nya itu None, f bakal dipanggil sekali. Karena semua closures mengimplementasikan FnOnce, unwrap_or_else bisa menerima ketiga jenis closures dan dia fleksibel sekali.

Catatan: Kalau hal yang mau kita lakukan tidak mewajibkan kita menangkap nilai dari lingkungannya, kita bisa aja memakai nama sebuah fungsi sebagai ganti dari closure di tempat kita butuh sesuatu yang mengimplementasikan salah satu dari trait Fn. Contohnya, pada nilai Option<Vec<T>>, kita bisa manggil unwrap_or_else(Vec::new) buat mendapatkan vector baru yang kosong kalau nilainya adalah None. Compiler bakal secara otomatis mengimplementasikan trait Fn yang paling pas buat definisi fungsi tersebut.

Sekarang mari kita lihat method standard library sort_by_key, yang didefinisikan pada slices, buat melihat bedanya dari unwrap_or_else dan kenapa sort_by_key memakai FnMut dan bukannya FnOnce buat trait bound-nya. Closure ini menerima satu argumen berupa referensi ke item saat ini yang ada di slice yang lagi diperiksa, lalu mengembalikan nilai bertipe K yang bisa diurutkan (ordered). Fungsi ini berguna pas kita mau mengurutkan sebuah slice berdasarkan atribut spesifik dari setiap itemnya. Di Listing 13-7, kita punya daftar berisi instance Rectangle dan kita memakai sort_by_key buat mengurutkan mereka berdasarkan atribut width (lebar) mereka dari yang terkecil sampai terbesar.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    list.sort_by_key(|r| r.width);
    println!("{list:#?}");
}
Listing 13-7: Memakai sort_by_key buat mengurutkan persegi panjang berdasarkan lebarnya

Kode ini mencetak:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
     Running `target/debug/rectangles`
[
    Rectangle {
        width: 3,
        height: 5,
    },
    Rectangle {
        width: 7,
        height: 12,
    },
    Rectangle {
        width: 10,
        height: 1,
    },
]

Alasan kenapa sort_by_key didefinisikan buat menerima closure FnMut adalah karena method itu memanggil closure-nya berkali-kali: satu kali untuk setiap item di slice-nya. Closure |r| r.width tidak menangkap, memutasi, atau memindahkan apa pun keluar dari lingkungannya, jadi ia memenuhi syarat trait bound-nya.

Sebaliknya, Listing 13-8 menunjukkan contoh closure yang cuma mengimplementasikan trait FnOnce, karena dia memindahkan sebuah nilai ke luar dari lingkungannya. Compiler tidak bakal ngebiarin kita pakai closure ini dengan sort_by_key.

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut sort_operations = vec![];
    let value = String::from("closure called");

    list.sort_by_key(|r| {
        sort_operations.push(value);
        r.width
    });
    println!("{list:#?}");
}
Listing 13-8: Mencoba memakai closure FnOnce bersama sort_by_key

Ini adalah cara aneh yang dibikin-bikin (dan tidak bisa jalan) buat mencoba menghitung berapa kali sort_by_key memanggil closure-nya saat ia mengurutkan list. Kode ini mencoba melakukan penghitungan ini dengan memasukkan (pushing) value—sebuah String dari lingkungan closure-nya—ke dalam vector sort_operations. Closure ini menangkap value lalu memindahkan value tersebut keluar dari closure dengan mentransfer kepemilikan (ownership) value ke vector sort_operations. Closure ini bisa dipanggil satu kali; mencoba memanggilnya untuk kedua kalinya tidak bakal berhasil karena value sudah tidak ada lagi di lingkungan closure-nya buat dimasukkan ke dalam sort_operations! Maka dari itu, closure ini cuma mengimplementasikan FnOnce. Pas kita mencoba men-compile kode ini, kita bakal dapat error yang bilang kalau value tidak bisa dipindahkan keluar dari closure karena closure-nya harus mengimplementasikan FnMut:

$ cargo run
   Compiling rectangles v0.1.0 (file:///projects/rectangles)
error[E0507]: cannot move out of `value`, a captured variable in an `FnMut` closure
  --> src/main.rs:18:30
   |
15 |     let value = String::from("closure called");
   |         -----   ------------------------------ move occurs because `value` has type `String`, which does not implement the `Copy` trait
   |         |
   |         captured outer variable
16 |
17 |     list.sort_by_key(|r| {
   |                      --- captured by this `FnMut` closure
18 |         sort_operations.push(value);
   |                              ^^^^^ `value` is moved here
   |
help: consider cloning the value if the performance cost is acceptable
   |
18 |         sort_operations.push(value.clone());
   |                                   ++++++++

For more information about this error, try `rustc --explain E0507`.
error: could not compile `rectangles` (bin "rectangles") due to 1 previous error

Error ini menunjuk ke baris di dalam isi closure yang memindahkan value keluar dari lingkungannya. Buat memperbaikinya, kita harus mengubah isi closure agar ia tidak memindahkan nilai-nilai keluar dari lingkungannya. Menyimpan sebuah variabel counter di lingkungan dan menambah nilainya dari dalam closure adalah cara yang jauh lebih masuk akal buat menghitung berapa kali closure tersebut dipanggil. Closure di Listing 13-9 berhasil jalan dengan sort_by_key karena ia cuma menangkap referensi mutable ke counter num_sort_operations dan makanya bisa dipanggil lebih dari sekali:

Filename: src/main.rs
#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

fn main() {
    let mut list = [
        Rectangle { width: 10, height: 1 },
        Rectangle { width: 3, height: 5 },
        Rectangle { width: 7, height: 12 },
    ];

    let mut num_sort_operations = 0;
    list.sort_by_key(|r| {
        num_sort_operations += 1;
        r.width
    });
    println!("{list:#?}, sorted in {num_sort_operations} operations");
}
Listing 13-9: Memakai closure FnMut bersama sort_by_key diperbolehkan

Traits Fn penting pas kita mendefinisikan atau memakai fungsi atau tipe yang memakai closures. Di bagian selanjutnya, kita bakal membahas iterators. Banyak method iterator yang menerima argumen closure, jadi tetap ingat detail-detail tentang closures ini ya saat kita lanjut belajar!