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

Advanced Functions (Fungsi Tingkat Lanjut) dan Closures

Bagian ini mengeksplorasi beberapa fitur tingkat lanjut yang berkaitan dengan fungsi dan closures, termasuk function pointers (pointer fungsi) dan mengembalikan closures.

Function Pointers

Kita udah ngebahas gimana caranya mengoper closures ke fungsi-fungsi; kita juga bisa mengoper fungsi biasa ke fungsi-fungsi lho! Teknik ini sangat berguna pas kita mau ngoper fungsi yang emang udah kita definisikan sebelumnya ketimbang harus ngebikin closure baru. Fungsi itu bisa dipaksa (coerce) menjadi tipe fn (dengan huruf f kecil), yang mana jangan sampai tertukar sama trait closure Fn. Tipe fn ini disebut sebagai function pointer. Mengoper fungsi memakai function pointers bakal memungkinkan kita buat memakai fungsi sebagai argumen buat fungsi yang lainnya.

Sintaks buat menentukan kalau sebuah parameter itu adalah function pointer itu mirip sama sintaks closures, kayak yang ditunjukin di Listing 20-28, di mana kita udah mendefinisikan sebuah fungsi add_one yang menjumlahkan 1 ke parameternya. Fungsi do_twice menerima dua parameter: sebuah function pointer ke fungsi mana aja yang nerima parameter i32 dan ngembaliin sebuah i32, dan satu nilai i32. Fungsi do_twice ini memanggil fungsi f sebanyak dua kali, mengoper nilai arg ke dalamnya, lalu menjumlahkan kedua hasil pemanggilan fungsi tersebut bersama-sama. Fungsi main lalu memanggil do_twice dengan argumen add_one dan 5.

Filename: src/main.rs
fn add_one(x: i32) -> i32 {
    x + 1
}

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(add_one, 5);

    println!("The answer is: {answer}");
}
Listing 20-28: Memakai tipe fn buat nerima function pointer sebagai argumen

Kode ini mencetak The answer is: 12. Kita menentukan kalau parameter f di dalam do_twice itu adalah sebuah fn yang nerima satu parameter bertipe i32 dan mengembalikan sebuah i32. Terus kita bisa manggil f di dalam isi (body) dari do_twice. Di main, kita bisa mengoper nama fungsi add_one sebagai argumen pertama ke do_twice.

Beda sama closures, fn itu adalah sebuah tipe bukannya trait, jadi kita menentukan fn sebagai tipe parameternya secara langsung ketimbang harus mendeklarasikan sebuah parameter tipe generik yang mana trait bounds-nya memakai salah satu dari trait Fn.

Function pointers mengimplementasikan ketiga trait closure sekaligus (Fn, FnMut, dan FnOnce), yang berarti kita bakal selalu bisa mengoper sebuah function pointer sebagai argumen buat fungsi yang membutuhkan sebuah closure. Praktik terbaiknya adalah nulis fungsi memakai tipe generik dan salah satu dari trait-trait closure tersebut supaya fungsi kita bisa nerima fungsi biasa maupun closures.

Namun, ada satu contoh di mana kita mungkin cuma mau nerima tipe fn aja dan tidak mau nerima closures, yaitu pas kita lagi berinteraksi (interfacing) dengan kode eksternal yang emang tidak punya closures: Fungsi-fungsi C bisa menerima fungsi biasa sebagai argumen, tapi C tidak punya fitur closures.

Sebagai contoh buat situasi di mana kita bisa memakai sebuah closure yang didefinisikan secara langsung (inline) atau sebuah fungsi bernama, mari kita lihat penggunaan method map yang disediain sama trait Iterator di standard library. Buat memakai method map buat mengubah sebuah vector angka-angka jadi vector string-string, kita bisa memakai closure, kayak di Listing 20-29.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(|i| i.to_string()).collect();
}
Listing 20-29: Memakai sebuah closure dengan method map buat mengubah angka jadi string

Atau kita juga bisa memakai fungsi bernama sebagai argumen buat method map ketimbang memakai sebuah closure. Listing 20-30 nunjukin bakal seperti apa kelihatannya kalau pakai cara ini.

fn main() {
    let list_of_numbers = vec![1, 2, 3];
    let list_of_strings: Vec<String> =
        list_of_numbers.iter().map(ToString::to_string).collect();
}
Listing 20-30: Memakai fungsi String::to_string bareng method map buat mengubah angka jadi string

Perhatikan bahwa kita wajib memakai fully qualified syntax (sintaks terkualifikasi penuh) yang udah kita obrolin di “Advanced Traits” karena ada banyak fungsi tersedia yang namanya sama-sama to_string.

Di sini, kita memakai fungsi to_string yang didefinisikan di dalam trait ToString, yang mana udah diimplementasikan secara otomatis sama standard library buat tipe apa aja yang mengimplementasikan trait Display.

Ingat kembali dari “Nilai Enum (Enum Values)” di Bab 6 kalau nama dari setiap varian enum yang kita definisikan itu juga bertindak sebagai sebuah fungsi inisialisasi (initializer function). Kita bisa memakai fungsi-fungsi inisialisasi ini sebagai function pointers yang mengimplementasikan trait-trait closure, yang berarti kita bisa mengoper fungsi inisialisasi tersebut sebagai argumen buat method-method yang nerima closures, kayak yang kelihatan di Listing 20-31.

fn main() {
    enum Status {
        Value(u32),
        Stop,
    }

    let list_of_statuses: Vec<Status> = (0u32..20).map(Status::Value).collect();
}
Listing 20-31: Memakai fungsi inisialisasi enum bareng method map buat ngebikin instance Status dari angka-angka

Di sini, kita ngebikin instance-instance Status::Value memakai setiap nilai u32 di dalam rentang yang mana dipanggil oleh method map dengan jalan memakai fungsi inisialisasi dari varian Status::Value tersebut. Beberapa orang lebih suka gaya penulisan kayak gini dan beberapa orang lainnya lebih milih buat pakai closures. Mereka di-compile jadi hasil kode yang sama kok, jadi pakai aja gaya mana yang lebih jelas dan enak dibaca buat kita.

Mengembalikan Closures

Closures direpresentasikan memakai traits, yang artinya kita tidak bisa ngembaliin closures secara langsung. Di sebagian besar kasus di mana kita mungkin pengen mengembalikan sebuah trait, kita bisa memakai tipe konkret yang emang mengimplementasikan trait tersebut sebagai nilai kembalian fungsinya. Tapi, biasanya kita tidak bisa ngelakuin itu buat closures karena mereka tidak punya tipe konkret yang bisa di-return (dikembalikan); contohnya, kita tidak diizinkan buat memakai function pointer fn sebagai tipe kembalian kalau closure-nya itu menangkap (captures) nilai-nilai apa pun dari scope-nya.

Sebaliknya, kita biasanya bakal memakai sintaks impl Trait yang udah kita pelajarin di Bab 10. Kita bisa ngembaliin tipe fungsi apa aja, dengan memakai Fn, FnOnce dan FnMut. Contohnya, kode di Listing 20-32 bakal sukses di-compile dengan baik.

#![allow(unused)]
fn main() {
fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}
}
Listing 20-32: Mengembalikan sebuah closure dari sebuah fungsi memakai sintaks impl Trait

Namun, kayak yang udah kita sebutin di “Inferensi Tipe dan Anotasi pada Closure” di Bab 13, masing-masing closure itu juga merupakan tipe mereka sendiri yang berbeda-beda. Kalau kita perlu beroperasi sama fungsi-fungsi yang punya signature yang sama tapi implementasi yang berbeda-beda, kita perlu memakai trait object buat mereka. Coba bayangin apa yang terjadi kalau kita nulis kode kayak yang ditunjukin di Listing 20-33.

Filename: src/main.rs
fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> impl Fn(i32) -> i32 {
    |x| x + 1
}

fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
    move |x| x + init
}
Listing 20-33: Membikin sebuah Vec<T> berisi closures yang didefinisikan sama fungsi-fungsi yang mengembalikan tipe impl Fn

Di sini kita punya dua fungsi, returns_closure dan returns_initialized_closure, yang mana dua-duanya mengembalikan impl Fn(i32) -> i32. Perhatikan kalau closures yang mereka kembalikan itu berbeda isinya, biarpun mereka mengimplementasikan tipe yang sama. Kalau kita mencoba men-compile ini, Rust bakal ngasih tahu kita kalau ini tidak bisa jalan:

$ cargo build
   Compiling functions-example v0.1.0 (file:///projects/functions-example)
error[E0308]: mismatched types
  --> src/main.rs:2:44
   |
 2 |     let handlers = vec![returns_closure(), returns_initialized_closure(123)];
   |                                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected opaque type, found a different opaque type
...
 9 | fn returns_closure() -> impl Fn(i32) -> i32 {
   |                         ------------------- the expected opaque type
...
13 | fn returns_initialized_closure(init: i32) -> impl Fn(i32) -> i32 {
   |                                              ------------------- the found opaque type
   |
   = note: expected opaque type `impl Fn(i32) -> i32`
              found opaque type `impl Fn(i32) -> i32`
   = note: distinct uses of `impl Trait` result in different opaque types

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

Pesan error ini ngasih tahu kita kalau kapan pun kita mengembalikan impl Trait, Rust ngebikin tipe opaque (buram/tidak tembus pandang) yang unik, yaitu sebuah tipe di mana kita tidak bisa melihat ke dalam detail dari apa yang dibangun sama Rust buat kita, dan kita juga tidak bisa nebak-nebak tipe apa yang bakal dihasilkan (generated) sama Rust supaya kita bisa nulisin tipenya sendiri secara manual. Jadi meskipun fungsi-fungsi ini sama-sama ngembaliin closures yang mengimplementasikan trait yang sama, yaitu Fn(i32) -> i32, tipe opaque yang dihasilkan oleh Rust buat masing-masing fungsinya itu tetap berbeda. (Ini mirip dengan gimana cara Rust memproduksi tipe konkret yang berbeda-beda untuk blok async yang berbeda bahkan pas mereka punya tipe output yang sama, kayak yang kita lihat di “Bekerja dengan Jumlah Futures yang Sembarang” di Bab 17.) Kita udah pernah lihat solusi buat masalah ini beberapa kali sebelumnya: kita bisa memakai sebuah trait object, kayak di Listing 20-34.

fn main() {
    let handlers = vec![returns_closure(), returns_initialized_closure(123)];
    for handler in handlers {
        let output = handler(5);
        println!("{output}");
    }
}

fn returns_closure() -> Box<dyn Fn(i32) -> i32> {
    Box::new(|x| x + 1)
}

fn returns_initialized_closure(init: i32) -> Box<dyn Fn(i32) -> i32> {
    Box::new(move |x| x + init)
}
Listing 20-34: Membikin sebuah Vec<T> berisi closures yang didefinisikan sama fungsi-fungsi yang mengembalikan Box<dyn Fn> supaya mereka punya tipe yang sama

Kode ini bakal sukses di-compile dengan mulus. Buat tahu lebih lanjut soal trait objects, silakan mengacu ke bagian “Memakai Trait Objects Buat Mengabstraksi Perilaku Bersama” di Bab 18.

Berikutnya, mari kita lihat tentang macro!