A.37. Traits ➜ Advanced
Chapter ini merupakan kelanjutan dari chapter sebelumnya. Di sini kita fokus ke pembuatan local trait, macam-macam implementasi traits, traits bound syntax, associated types, dan topik yang masih relevan dengan traits lainnya.
Pembahasan lanjutan mengenai traits ini cukup panjang. Makin mendekati akhir pembahasan, makin berat topik yang dibahas. Penulis anjurkan jika diperlukan, silakan mengulang-ulang chapter ini, atau bisa lanjut ke chapter berikutnya dan nanti kembali lagi ke sini ketika sudah siap.
A.37.1. Membuat custom trait (local trait)
Pada bagian ini kita akan belajar tentang cara membuat definisi trait di local crate dalam project yang sedang dikerjakan.
Masih sama dengan sebelum-sebelumnya, pembelajaran dilakukan dengan praktik. Kita gunakan skenario praktik berikut pada program selanjutnya:
- Buat struct bernama
Circle
. - Buat struct bernama
Square
. - Buat trait bernama
Area
dengan isi satu buah method untuk menghitung luas bangun datar (methodcalculate_area()
). - Implementasikan trait
Area
ke dua struct tersebut.
Ok, mari mulai praktikkan skenario di atas. Pertama siapkan project dengan struktur berikut:
my_package
│─── Cargo.toml
└─── src
│─── calculation_spec.rs
│─── two_dimensional.rs
└─── main.rs
Module calculation_spec
berisi definisi trait Area
. Trait ini punya visibility akses publik, isinya hanya satu buah definisi method header bernama calculate_area()
. Trait ini nantinya diimplementasikan ke struct Circle
dan juga Square
, agar nantinya kedua struct tersebut memiliki method calculate_area()
yang berguna untuk kalkulasi luas bangun datar 2d.
pub trait Area {
fn calculate_area(&self) -> f64;
}
Kemudian siapkan file two_dimensional
, isinya dua buah struct: Circle
dan Square
. Pada file yang sama, siapkan juga block kode implementasi trait Area
. Dengan ini maka kedua struct tersebut wajib untuk memiliki method bernama calculate_area()
dengan isi adalah operasi perhitungan aritmatika untuk mencari luas bangun datar.
pub struct Circle {
pub radius: i32,
}
impl crate::calculation_spec::Area for Circle {
fn calculate_area(&self) -> f64 {
// PI * (r ^ 2)
// ada operasi casting ke tipe f64 karena self.radius bertipe i32
3.14 * (self.radius.pow(2) as f64)
}
}
pub struct Square {
pub length: i32,
}
impl crate::calculation_spec::Area for Square {
fn calculate_area(&self) -> f64 {
// (s ^ 2)
// ada operasi casting ke tipe f64 karena self.length bertipe i32
self.length.pow(2) as f64
}
}
Bisa dilihat pada kode di atas, deklarasi struct beserta property memiliki visibility publik. Idealnya, saat sturct tersebut digunakan di fungsi main
nantinya tidak akan ada error terkait visibility akses.
Selanjutnya, pada file main.rs
siapkan kode yang isinya registrasi module calculate_spec
dan two_dimensional
, juga definisi fungsi main
dengan isi statement pembuatan 2 variabel object untuk masing-masing tipe data struct Circle
dan Square
.
mod calculation_spec;
mod two_dimensional;
fn main() {
let circle_one = two_dimensional::Circle{ radius: 10 };
println!("circle area: {}", circle_one.calculate_area());
let square_one = two_dimensional::Square{ length: 5 };
println!("square area: {}", square_one.calculate_area());
}
Method calculate_area()
milik object bertipe Circle
dan Square
diakses untuk kemudian di-print.
Coba jalankan program.
Hmm, error. Padahal trait Area
sudah publik, dan struct Circle
& Square
beserta property-nya juga sudah publik. Tapi masih error.
Error ini disebabkan oleh trait Area
yang belum di-import di crate root (main). Meskipun kita tidak mengakses trait tersebut secara langsung (melainkan via method calculate_area()
milik struct Circle
dan Square
), diharuskan untuk meng-import-nya juga.
Detail error beserta solusi dari error ini sebenarnya bisa dilihat di error message. Bagaimana Rust menginformasikan error sangat luar biasa informatif.
Ok, sekarang ubah isi file main.rs
menjadi seperti ini, kemudian jalankan ulang program. Hasilnya tidak ada error.
mod calculation_spec;
mod two_dimensional;
use crate::calculation_spec::Area; // <------- tambahkan statement import module
fn main() {
let circle_one = two_dimensional::Circle{ radius: 10 };
println!("circle area: {}", circle_one.calculate_area());
let square_one = two_dimensional::Square{ length: 5 };
println!("square area: {}", square_one.calculate_area());
}
O iya, ada beberapa hal baru pada penerapan kode di atas, berikut adalah pembahasannya:
◉ Method pow
untuk operasi pangkat
Method pow
adalah item milik tipe data numerik (i8
, i16
, i32
, ...) yang fungsinya untuk operasi pangkat.
3.pow(2); // ===> 3 pangkat 2
8.pow(5); // ===> 8 pangkat 5
◉ Keyword as
untuk casting tipe data
Keyword as
digunakan untuk casting tipe data. Keyword ini bisa diterapkan pada beberapa jenis tipe data, salah satunya adalah semua tipe data numerik.
1024 as f32; // ===> 1024 dikonversi ke tipe f32, hasinya adalah 1024.0
3.14 as i32; // ===> 3.14 dikonversi ke tipe i32, hasinya 3 karena ada pembulatan
A.37.2. Trait sebagai tipe parameter
Trait bisa digunakan sebagai tipe data parameter sebuah fungsi, contoh notasi penulisannya bisa dilihat pada kode berikut:
fn calculate_and_print_result(name: String, item: &impl Area) {
println!("{} area: {}", name, item.calculate_area());
}
Manfaat penerapan trait sebagai tipe data parameter fungsi adalah saat pemanggilan fungsi, parameter tersebut bisa diisi dengan argument data bertipe apapun dengan catatan tipe dari data tersebut mengimplementasikan trait yang sama dengan yang digunakan pada parameter.
Contohnya seperti pada fungsi calculate_and_print_result
di atas yang parameter ke-2 bertipe &impl Area
, nantinya saat fungsi tersebut dipanggil, kita bisa sisipi parameter ke-2 dengan object circle_one
ataupun circle_two
.
let circle_one = two_dimensional::Circle{ radius: 10 };
calculate_and_print_result("circle".to_string(), &circle_one);
let square_one = two_dimensional::Square{ length: 5 };
calculate_and_print_result("square".to_string(), &square_one);
&impl Area
ini tipe data pointer ya, tipe non-pointer-nya adalahimpl Area
. Di sini digunakan tipe data pointer untuk antisipasi move semantics pada tipe data custom type (borrowing).
A.37.3. Parameter bertipe lebih dari 1 trait
Bagaimana jika, parameter fungsi item
di atas tipenya bisa lebih dari 1 trait, misalnya trait Area
dan Circumference
, apakah bisa? Jawabannya bisa.
Mari praktekan. Lakukan modifikasi berikut pada beberapa kode yang telah di tulis.
File
src/calculation_spec.rs
// ...
// tambahkan ⬇️
pub trait Circumference {
fn calculate_circumference(&self) -> f64;
}File
src/two_dimensional.rs
// ...
// tambahkan ⬇️
impl crate::calculation_spec::Circumference for Circle {
fn calculate_circumference(&self) -> f64 {
2.0 * 3.14 * (self.radius) as f64
}
}
impl crate::calculation_spec::Circumference for Square {
fn calculate_circumference(&self) -> f64 {
4.0 * (self.length) as f64
}
}File
src/main.rs
// ...
// tambahkan ⬇️
use crate::calculation_spec::Circumference;
// ...
fn calculate_and_print_result(name: String, item: &(impl Area + Circumference)) {
println!("{} area: {}", name, item.calculate_area());
println!("{} circumference: {}", name, item.calculate_circumference());
}
Dengan perubahan kode yang telah dilakukan di atas, parameter item
milik fungsi calculate_and_print_result()
bisa diisi dengan nilai argument apapun asalkan memenuhi kriteria tipe yang ditentukan, yaitu: haruse meng-implement trait Area
dan trait Circumference
.
Notasi penulisan tipe-datanya &(impl Trait1 + Trait2 + Trait3 + ...)
.
A.37.4. Trait bound syntax
Penerapan trait sebagai parameter fungsi juga bisa dituliskan dalam notasi yang memanfaatkan generic. Teknik penulisan ini disebut dengan trait bound syntax.
Contohnya bisa dilihat pada kode berikut. Ada generic bernama T
yang merepresentasikan trait Area
, kemudian pada definisi parameter ke-2 fungsi (yaitu parameter item
) tipenya menggunakan &T
. Tipe &T
di sini adalah ekuivalen dengan &impl Area
.
fn calculate_and_print_result2<T: Area>(name: String, item: &T) {
println!("{} area: {}", name, item.calculate_area());
}
Jika ada lebih dari satu trait yang digunakan sebagai tipe data paramater, maka penulisannya seperti ini:
fn calculate_and_print_result3<T: Area + Circumference>(name: String, item: &T) {
println!("{} area: {}", name, item.calculate_area());
println!("{} circumference: {}", name, item.calculate_circumference());
}
Satu tambahan contoh lagi untuk ilustrasi yang lebih kompleks:
fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {
// ...
}
Pada contoh di atas fungsi some_function()
memiliki 2 generics param, yaitu T
dan U
.
T
merepresentasikan traitDisplay
danClone
U
merepresentasikan traitClone
danDebug
Lebih jelasnya mengenai generics dibahas pada chapter Generics
A.37.5. Trait where
clause
Ada lagi alternatif penulisan trait bound syntax, yaitu menggunakan keyword where
. Contoh pengaplikasiannya bisa dilihat pada kode berikut. Semua definisi fungsi di bawah ini adalah ekuivalen.
fn calculate_and_print_result4<T>(name: String, item: &T) where T: Area + Circumference {
println!("{} area: {}", name, item.calculate_area());
}
fn calculate_and_print_result5<T>(name: String, item: &T)
where
T: Area + Circumference,
// ... other generic params if exists
{
println!("{} area: {}", name, item.calculate_area());
}
Lebih jelasnya mengenai generics dibahas pada chapter Generics
A.37.6. Trait sebagai return type
Trait bisa juga digunakan sebagai tipe data return value. Caranya gunakan notasi penulisan impl NamaTrait
sebagai tipe data.
Contohnya bisa dilihat pada kode berikut. Ada dua fungsi baru dideklarasikan:
- Fungsi
new_circle
dengan return type adalahimpl Area
, dan data yang dikembalikan adalah bertipetwo_dimensional::Circle
. - Fungsi
new_square
dengan return type adalahimpl Area + Circumference
, dan data yang dikembalikan adalah bertipetwo_dimensional::Square
.
fn main() {
let circle_one = new_circle(5);
calculate_and_print_result6("circle".to_string(), &circle_one);
let square_one = new_square(10);
calculate_and_print_result7("square".to_string(), &square_one);
}
fn new_circle(radius: i32) -> impl Area {
let data = two_dimensional::Circle{
radius
};
data
}
fn new_square(length: i32) -> impl Area + Circumference {
two_dimensional::Square{
length
}
}
fn calculate_and_print_result6<T>(name: String, item: &T)
where
T: Area,
{
println!("{} area: {}", name, item.calculate_area());
}
fn calculate_and_print_result7<T>(name: String, item: &T)
where
T: Area + Circumference,
{
println!("{} area: {}", name, item.calculate_area());
println!("{} circumference: {}", name, item.calculate_circumference());
}
Salah satu konsekuensi dalam penerapan trait sebagai return type adalah: tipe data milik nilai yang dikembalikan terdeteksi sebagai tipe trait. Contohnya variabel circle_one
di atas, tipe data-nya bukan Circle
, melainkan impl Area
.
Tipe data aslinya tetap bisa diakses, tapi butuh tambahan effort. Lebih jelasnya dibahas pada chapter Trait ➜ Conversion (From & Into).
A.37.7. Associated types pada trait
Associated types adalah tipe data yang didefinisikan di dalam suatu trait. Associated types tidak tidak memiliki tipe data konkret saat didefinisikan, namun ketika trait di-implementasikan maka tipe tersebut harus ditentukan tipe data konkritnya.
Lebih jelas silakan perhatikan kode berikut:
trait Shape {
type Area;
fn area(&self) -> Self::Area;
}
Pada definisi trait Shape
di atas, yang disebut dengan associated types adalah tipe Area
yang definisinya berada dalam block trait. Tipe didefinisikan tanpa assignment operator, jadi tidak ada tipe data konkretnya.
Associated types ini sering digunakan pada Rust programming.
Lanjut skenario praktek berikutnya. Silakan siapkan package/project baru dengan struktur seperti ini:
my_package
│─── Cargo.toml
└─── src
│─── shape.rs
│─── circle.rs
│─── square.rs
└─── main.rs
- Disiapkan suatu trait bernama
shape::Shape
.- Trait ini memiliki satu associated types bernama
Area
. - Dan memiliki sebuah definisi method header
area
yang gunanya untuk menghitung luas bangun datar (shape).
- Trait ini memiliki satu associated types bernama
- Disiapkan struct
circle::Circle
yang mengadopsi traitshape::Shape
. - Disiapkan struct
square::Square
yang mengadopsi traitshape::Shape
.
pub trait Shape {
type Area;
fn area(&self) -> Self::Area;
}
Trait Shape
di atas spesifikasinya mirip seperti pada contoh sebelumnya, hanya saja kali ini trait-nya di set public agar bisa diakses dari main.rs
nantinya.
Trait Shape
kemudian di-implementasikan ke struct Circle
dan Square
, kode-nya bisa dilihat berikut:
pub struct Circle {
pub radius: f64,
}
impl crate::shape::Shape for Circle {
type Area = f64;
fn area(&self) -> Self::Area {
std::f64::consts::PI * self.radius * self.radius
}
}
pub struct Square {
pub side: i64,
}
impl crate::shape::Shape for Square {
type Area = i64;
fn area(&self) -> Self::Area {
self.side * self.side
}
}
Bisa dilihat pada kedua implementasi di atas, associated type Area
diisi dengan tipe concrete, yaitu:
- Tipe data
f64
sebagai tipe concretecircle:Circle:Area
- Tipe data
i64
sebagai tipe concretesquare:Square:Area
Contoh di atas adalah cara pengaplikasian associated types.
Lalu pada main.rs
, tipe data struct circle::Circle
dan square::Square
digunakan untuk membuat variabel baru, yang kemudian dari variabel tersebut, method .area()
milik diakses.
mod shape;
mod circle;
mod square;
use crate::shape::Shape;
fn main() {
let obj1 = circle::Circle{ radius: 10.0 };
println!("area of circle: {:.2}", obj1.area());
let obj2 = square::Square{ side: 10 };
println!("area of square: {:}", obj2.area());
}
Silakan jalankan program dan lihat hasilnya.
O iya, pada main.rs
, module item shape::Shape
perlu di-import meskipun kita tidak menggunakan trait
tersebut secara langsung. Jika tidak di-import, maka method .area()
milik Circle
dan Square
tidak bisa diakses.
A.37.8. Attribute derive
Ada cara lain untuk mengimplementasikan suatu trait ke dalam tipe data selain dengan menuliskan implementasinya secara eksplist, caranya menggunakan attribute derive
.
Lebih detailnya dibahas pada chapter Attributes.
Catatan chapter 📑
◉ Source code praktik
github.com/novalagung/dasarpemrogramanrust-example/../advanced_traits
◉ Work in progress
- Pembahasan tentang trait bounds untuk implementasi method kondisional
- Pembahasan tentang trait overloading
- orphan rules