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.
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}");
}
fn buat nerima function pointer sebagai argumenKode 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();
}
map buat mengubah angka jadi stringAtau 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();
}
String::to_string bareng method map buat mengubah angka jadi stringPerhatikan 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();
}
map buat ngebikin instance Status dari angka-angkaDi 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
}
}
impl TraitNamun, 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.
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
}
Vec<T> berisi closures yang didefinisikan sama fungsi-fungsi yang mengembalikan tipe impl FnDi 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)
}
Vec<T> berisi closures yang didefinisikan sama fungsi-fungsi yang mengembalikan Box<dyn Fn> supaya mereka punya tipe yang samaKode 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!