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

Traits: Mendefinisikan Perilaku Bersama

Sebuah trait mendefinisikan fungsionalitas yang dimiliki suatu tipe tertentu dan bisa dibagikan (di-share) dengan tipe lainnya. Kita bisa memakai traits buat mendefinisikan perilaku bersama (shared behavior) secara abstrak. Kita bisa memakai trait bounds buat menentukan kalau sebuah tipe generik bisa berupa tipe apa pun asalkan punya perilaku tertentu.

Catatan: Traits itu mirip sama fitur yang sering disebut interfaces di bahasa pemrograman lain, walaupun ada beberapa perbedaan.

Mendefinisikan sebuah Trait

Perilaku dari sebuah tipe terdiri dari methods yang bisa kita panggil pada tipe tersebut. Berbagai tipe bisa berbagi perilaku yang sama kalau kita bisa memanggil methods yang sama pada semua tipe itu. Definisi trait adalah cara buat mengelompokkan method signatures (tanda tangan metode) bersama-sama untuk mendefinisikan sekumpulan perilaku yang dibutuhkan untuk mencapai suatu tujuan.

Misalnya, katakanlah kita punya beberapa struct yang menampung berbagai macam dan jumlah teks: sebuah struct NewsArticle yang menampung berita di lokasi tertentu dan sebuah SocialPost yang maksimal isinya 280 karakter beserta metadata yang menunjukkan apakah itu postingan baru, di-repost, atau balasan buat postingan lain.

Kita mau bikin library crate agregator media bernama aggregator yang bisa nampilin ringkasan data yang mungkin disimpan di dalam instance NewsArticle atau SocialPost. Untuk melakukan ini, kita butuh ringkasan dari tiap tipe, dan kita bakal minta ringkasan itu dengan memanggil method summarize di tiap instance-nya. Listing 10-12 menunjukkan definisi trait publik Summary yang mengekspresikan perilaku ini.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}
Listing 10-12: Sebuah trait Summary yang terdiri dari perilaku yang disediakan oleh method summarize

Di sini, kita mendeklarasikan sebuah trait memakai keyword trait lalu nama trait-nya, yang mana adalah Summary di kasus ini. Kita juga mendeklarasikan trait ini sebagai pub supaya crates yang bergantung pada crate ini bisa memanfaatkan trait ini juga, seperti yang bakal kita lihat di beberapa contoh nanti. Di dalam kurung kurawal, kita mendeklarasikan method signatures yang menggambarkan perilaku tipe-tipe yang mengimplementasikan trait ini, yang di kasus ini adalah fn summarize(&self) -> String.

Setelah method signature, bukannya ngasih implementasi di dalam kurung kurawal, kita memakai titik koma. Tiap tipe yang mengimplementasikan trait ini harus menyediakan perilaku khususnya sendiri buat body (isi) dari method ini. Compiler bakal memastikan kalau tipe apa pun yang punya trait Summary bakal punya method summarize yang didefinisikan dengan signature yang persis sama kayak gini.

Sebuah trait bisa punya banyak method di dalamnya: method signatures didaftarkan satu baris satu, dan tiap baris diakhiri dengan titik koma.

Mengimplementasikan sebuah Trait pada suatu Tipe

Sekarang setelah kita mendefinisikan signatures yang diinginkan dari method trait Summary, kita bisa mengimplementasikannya pada tipe-tipe di agregator media kita. Listing 10-13 menunjukkan implementasi trait Summary pada struct NewsArticle yang memakai judul (headline), penulis, dan lokasi buat bikin nilai kembalian dari summarize. Buat struct SocialPost, kita mendefinisikan summarize sebagai username diikuti sama seluruh teks postingannya, dengan asumsi kalau konten postingan sudah dibatasi sampai 280 karakter.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-13: Mengimplementasikan trait Summary pada tipe NewsArticle dan SocialPost

Mengimplementasikan trait pada suatu tipe itu mirip dengan mengimplementasikan method biasa. Bedanya adalah setelah impl, kita menaruh nama trait yang mau kita implementasikan, lalu memakai keyword for, dan kemudian menentukan nama tipe di mana kita mau mengimplementasikan trait tersebut. Di dalam blok impl, kita menaruh method signatures yang sudah didefinisikan sama definisi trait-nya. Alih-alih menambahkan titik koma setelah setiap signature, kita memakai kurung kurawal dan mengisi isi method dengan perilaku spesifik yang kita mau dari method trait tersebut untuk tipe khususnya.

Sekarang setelah library ini mengimplementasikan trait Summary pada NewsArticle dan SocialPost, pengguna dari crate ini bisa memanggil method dari trait tersebut pada instance NewsArticle dan SocialPost dengan cara yang sama seperti memanggil method biasa. Bedanya cuma si pengguna harus membawa trait tersebut ke dalam scope sekaligus membawa tipe-tipenya. Ini contoh gimana sebuah binary crate bisa memakai library crate aggregator kita:

use aggregator::{SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Kode ini bakal mencetak 1 new post: horse_ebooks: of course, as you probably already know, people.

Crates lain yang bergantung pada crate aggregator juga bisa membawa trait Summary ke dalam scope untuk mengimplementasikan Summary di tipe mereka sendiri. Satu batasan yang perlu dicatat adalah kita cuma bisa mengimplementasikan sebuah trait pada suatu tipe kalau setidaknya trait-nya atau tipenya, atau keduanya, berada di crate kita sendiri (local to our crate). Misalnya, kita bisa mengimplementasikan trait dari standard library seperti Display pada tipe kustom seperti SocialPost sebagai bagian dari fungsionalitas crate aggregator kita karena tipe SocialPost itu ada di crate aggregator kita. Kita juga bisa mengimplementasikan Summary pada Vec<T> di crate aggregator kita karena trait Summary itu ada di crate aggregator kita.

Tapi kita tidak bisa mengimplementasikan traits eksternal pada tipe eksternal. Misalnya, kita tidak bisa mengimplementasikan trait Display pada Vec<T> di dalam crate aggregator kita karena Display dan Vec<T> dua-duanya didefinisikan di standard library dan bukan bagian dari crate aggregator kita. Batasan ini adalah bagian dari properti yang disebut coherence (koherensi), dan lebih spesifik lagi disebut orphan rule (aturan yatim piatu), dinamai begitu karena tipe induknya tidak ada. Aturan ini memastikan kalau kode milik orang lain tidak bisa merusak kode kita dan sebaliknya. Tanpa aturan ini, dua crates bisa saja mengimplementasikan trait yang sama untuk tipe yang sama, dan Rust tidak bakal tau implementasi mana yang harus dipakai.

Implementasi Default

Kadang-kadang akan berguna kalau kita punya perilaku default untuk beberapa atau semua method di sebuah trait daripada mewajibkan implementasi untuk semua method di setiap tipe. Dengan begitu, saat kita mengimplementasikan trait pada tipe tertentu, kita bisa tetap menyimpan atau menimpa (override) perilaku default dari tiap method.

Di Listing 10-14, kita menentukan string default buat method summarize dari trait Summary alih-alih cuma mendefinisikan method signature-nya, seperti yang kita lakukan di Listing 10-12.

Filename: src/lib.rs
pub trait Summary {
    fn summarize(&self) -> String {
        String::from("(Read more...)")
    }
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}
Listing 10-14: Mendefinisikan trait Summary dengan implementasi default buat method summarize

Untuk memakai implementasi default buat meringkas instance dari NewsArticle, kita cukup menentukan blok impl yang kosong dengan impl Summary for NewsArticle {}.

Meskipun kita tidak lagi mendefinisikan method summarize di NewsArticle secara langsung, kita sudah menyediakan implementasi default dan menentukan kalau NewsArticle mengimplementasikan trait Summary. Hasilnya, kita tetap bisa memanggil method summarize pada instance NewsArticle, seperti ini:

use aggregator::{self, NewsArticle, Summary};

fn main() {
    let article = NewsArticle {
        headline: String::from("Penguins win the Stanley Cup Championship!"),
        location: String::from("Pittsburgh, PA, USA"),
        author: String::from("Iceburgh"),
        content: String::from(
            "The Pittsburgh Penguins once again are the best \
             hockey team in the NHL.",
        ),
    };

    println!("New article available! {}", article.summarize());
}

Kode ini mencetak New article available! (Read more...).

Membuat implementasi default tidak mengharuskan kita untuk mengubah apa pun dari implementasi Summary pada SocialPost di Listing 10-13. Alasannya adalah sintaks buat menimpa implementasi default itu persis sama kayak sintaks buat mengimplementasikan method trait yang tidak punya implementasi default.

Implementasi default bisa memanggil method lain di trait yang sama, bahkan kalau method lain itu tidak punya implementasi default. Dengan cara ini, sebuah trait bisa menyediakan banyak fungsionalitas berguna dan cuma mewajibkan si peng-implementasi buat menentukan sebagian kecil saja. Misalnya, kita bisa mendefinisikan trait Summary agar punya method summarize_author yang implementasinya wajib, lalu mendefinisikan method summarize yang punya implementasi default yang memanggil method summarize_author:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Untuk memakai versi Summary ini, kita cuma perlu mendefinisikan summarize_author pas kita mengimplementasikan trait-nya pada sebuah tipe:

pub trait Summary {
    fn summarize_author(&self) -> String;

    fn summarize(&self) -> String {
        format!("(Read more from {}...)", self.summarize_author())
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize_author(&self) -> String {
        format!("@{}", self.username)
    }
}

Setelah kita mendefinisikan summarize_author, kita bisa memanggil summarize pada instance dari struct SocialPost, dan implementasi default dari summarize bakal memanggil definisi summarize_author yang sudah kita sediakan. Karena kita sudah mengimplementasikan summarize_author, trait Summary sudah ngasih kita perilaku dari method summarize tanpa mengharuskan kita menulis kode tambahan lagi. Berikut contoh pemakaiannya:

use aggregator::{self, SocialPost, Summary};

fn main() {
    let post = SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    };

    println!("1 new post: {}", post.summarize());
}

Kode ini mencetak 1 new post: (Read more from @horse_ebooks...).

Perhatikan kalau tidak mungkin untuk memanggil implementasi default dari dalam implementasi yang lagi menimpa (overriding) method yang sama.

Traits sebagai Parameter

Sekarang setelah kita tahu cara mendefinisikan dan mengimplementasikan traits, kita bisa eksplor gimana cara memakai traits buat mendefinisikan fungsi yang bisa menerima berbagai macam tipe. Kita bakal memakai trait Summary yang sudah kita implementasikan di tipe NewsArticle dan SocialPost di Listing 10-13 untuk mendefinisikan fungsi notify yang memanggil method summarize pada parameter item-nya, yang bertipe apa pun selama tipe itu mengimplementasikan trait Summary. Buat melakukannya, kita memakai sintaks impl Trait, seperti ini:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

pub fn notify(item: &impl Summary) {
    println!("Breaking news! {}", item.summarize());
}

Alih-alih tipe konkret buat parameter item, kita memakai keyword impl bersama dengan nama trait-nya. Parameter ini bakal menerima tipe apa pun yang mengimplementasikan trait yang ditentukan. Di dalam body dari notify, kita bisa memanggil method apa pun pada item yang asalnya dari trait Summary, contohnya summarize. Kita bisa memanggil notify dan memberikan instance apa pun dari NewsArticle atau SocialPost. Kode yang memanggil fungsi tersebut dengan tipe lain, misalnya String atau i32, tidak bakal bisa di-compile karena tipe-tipe tersebut tidak mengimplementasikan Summary.

Sintaks Trait Bound

Sintaks impl Trait memang praktis buat kasus-kasus sederhana tapi sebenarnya itu cuma syntax sugar (sintaks pemanis) dari bentuk yang lebih panjang yang dikenal sebagai trait bound; bentuknya kayak gini:

pub fn notify<T: Summary>(item: &T) {
    println!("Breaking news! {}", item.summarize());
}

Bentuk yang lebih panjang ini ekuivalen (sama) dengan contoh di bagian sebelumnya, tapi lebih panjang (verbose). Kita menaruh trait bounds bersamaan dengan deklarasi parameter tipe generik setelah tanda titik dua (:) dan di dalam kurung sudut.

Sintaks impl Trait itu nyaman dan bikin kode lebih ringkas buat kasus-kasus sederhana, sementara sintaks trait bound yang lebih lengkap bisa mengekspresikan lebih banyak kerumitan buat kasus lain. Misalnya, kita bisa punya dua parameter yang dua-duanya mengimplementasikan Summary. Kalau pakai sintaks impl Trait, bentuknya bakal seperti ini:

pub fn notify(item1: &impl Summary, item2: &impl Summary) {

Memakai impl Trait cocok kalau kita mau fungsi ini mengizinkan item1 dan item2 untuk punya tipe yang berbeda (asalkan dua-duanya mengimplementasikan Summary). Tapi, kalau kita mau memaksa kedua parameter tersebut buat punya tipe yang sama persis, kita harus memakai trait bound, seperti ini:

pub fn notify<T: Summary>(item1: &T, item2: &T) {

Tipe generik T yang ditentukan sebagai tipe dari parameter item1 dan item2 membatasi fungsi ini sehingga tipe konkret dari nilai yang diberikan buat argumen item1 dan item2 itu harus sama.

Menentukan Beberapa Trait Bounds dengan Sintaks +

Kita juga bisa menentukan lebih dari satu trait bound. Katakanlah kita mau notify bisa memakai display formatting di samping memanggil summarize pada item: kita tentukan di definisi notify kalau item harus mengimplementasikan Display sekaligus Summary. Kita bisa melakukannya menggunakan sintaks +:

pub fn notify(item: &(impl Summary + Display)) {

Sintaks + ini juga valid buat dipakai sama trait bounds pada tipe generik:

pub fn notify<T: Summary + Display>(item: &T) {

Dengan dua trait bounds yang ditentukan, body dari notify bisa memanggil summarize dan juga memakai {} buat memformat item.

Trait Bounds yang Lebih Rapi pake Klausa where

Memakai terlalu banyak trait bounds ada sisi negatifnya. Masing-masing generik punya trait bounds-nya sendiri, jadi fungsi dengan banyak parameter tipe generik bisa mengandung sangat banyak informasi trait bound di antara nama fungsi dan daftar parameternya, yang mana bisa bikin signature fungsinya jadi susah dibaca. Karena alasan ini, Rust punya sintaks alternatif buat menentukan trait bounds di dalam sebuah klausa where setelah signature fungsinya. Jadi, alih-alih nulis begini:

fn some_function<T: Display + Clone, U: Clone + Debug>(t: &T, u: &U) -> i32 {

kita bisa pakai klausa where, kayak gini:

fn some_function<T, U>(t: &T, u: &U) -> i32
where
    T: Display + Clone,
    U: Clone + Debug,
{
    unimplemented!()
}

Signature fungsinya jadi tidak terlalu penuh: nama fungsi, daftar parameter, dan tipe kembalian semuanya berdekatan, mirip seperti fungsi yang tidak punya banyak trait bounds.

Mengembalikan Tipe yang Mengimplementasikan Traits

Kita juga bisa memakai sintaks impl Trait di posisi kembalian (return position) buat mengembalikan nilai dari suatu tipe yang mengimplementasikan sebuah trait, kayak gini:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable() -> impl Summary {
    SocialPost {
        username: String::from("horse_ebooks"),
        content: String::from(
            "of course, as you probably already know, people",
        ),
        reply: false,
        repost: false,
    }
}

Dengan memakai impl Summary buat tipe kembaliannya, kita menentukan kalau fungsi returns_summarizable bakal mengembalikan suatu tipe yang mengimplementasikan trait Summary tanpa harus menyebut nama tipe konkretnya. Di kasus ini, returns_summarizable mengembalikan sebuah SocialPost, tapi kode yang memanggil fungsi ini tidak perlu tau soal itu.

Kemampuan buat menentukan tipe kembalian hanya berdasarkan trait yang diimplementasikannya itu sangat berguna, apalagi di konteks closures dan iterators, yang bakal kita bahas di Bab 13. Closures dan iterators bikin tipe-tipe yang cuma compiler doang yang tau, atau tipe-tipe yang namanya kepanjangan buat ditulis. Sintaks impl Trait memudahkan kita menentukan secara ringkas kalau sebuah fungsi mengembalikan tipe tertentu yang mengimplementasikan trait Iterator tanpa perlu nulis tipe yang kepanjangan.

Tapi, kita cuma bisa memakai impl Trait kalau kita mengembalikan satu tipe tunggal. Misalnya, kode ini, yang mengembalikan entah NewsArticle atau SocialPost dengan tipe kembalian impl Summary, tidak bakal bisa jalan:

pub trait Summary {
    fn summarize(&self) -> String;
}

pub struct NewsArticle {
    pub headline: String,
    pub location: String,
    pub author: String,
    pub content: String,
}

impl Summary for NewsArticle {
    fn summarize(&self) -> String {
        format!("{}, by {} ({})", self.headline, self.author, self.location)
    }
}

pub struct SocialPost {
    pub username: String,
    pub content: String,
    pub reply: bool,
    pub repost: bool,
}

impl Summary for SocialPost {
    fn summarize(&self) -> String {
        format!("{}: {}", self.username, self.content)
    }
}

fn returns_summarizable(switch: bool) -> impl Summary {
    if switch {
        NewsArticle {
            headline: String::from(
                "Penguins win the Stanley Cup Championship!",
            ),
            location: String::from("Pittsburgh, PA, USA"),
            author: String::from("Iceburgh"),
            content: String::from(
                "The Pittsburgh Penguins once again are the best \
                 hockey team in the NHL.",
            ),
        }
    } else {
        SocialPost {
            username: String::from("horse_ebooks"),
            content: String::from(
                "of course, as you probably already know, people",
            ),
            reply: false,
            repost: false,
        }
    }
}

Mengembalikan entah NewsArticle atau SocialPost itu tidak diperbolehkan karena adanya batasan dari gimana sintaks impl Trait diimplementasikan di dalam compiler. Kita bakal bahas gimana cara nulis fungsi dengan perilaku kayak gini di bagian “Memakai Trait Objects yang Mengizinkan Nilai Dari Tipe yang Berbeda-beda” di Bab 18.

Memakai Trait Bounds Buat Mengimplementasikan Method secara Bersyarat

Dengan memakai trait bound bareng sebuah blok impl yang memakai parameter tipe generik, kita bisa mengimplementasikan methods secara bersyarat (conditionally) buat tipe-tipe yang mengimplementasikan traits yang ditentukan. Misalnya, tipe Pair<T> di Listing 10-15 selalu mengimplementasikan fungsi new buat mengembalikan instance baru dari Pair<T> (ingat dari bagian “Mendefinisikan Methods” di Bab 5 bahwa Self adalah alias tipe buat tipe dari blok impl-nya, yang mana di kasus ini adalah Pair<T>). Tapi di blok impl berikutnya, Pair<T> cuma mengimplementasikan method cmp_display kalau tipe di dalamnya T mengimplementasikan trait PartialOrd (yang memungkinkan perbandingan) dan trait Display (yang memungkinkan untuk dicetak).

Filename: src/lib.rs
use std::fmt::Display;

struct Pair<T> {
    x: T,
    y: T,
}

impl<T> Pair<T> {
    fn new(x: T, y: T) -> Self {
        Self { x, y }
    }
}

impl<T: Display + PartialOrd> Pair<T> {
    fn cmp_display(&self) {
        if self.x >= self.y {
            println!("The largest member is x = {}", self.x);
        } else {
            println!("The largest member is y = {}", self.y);
        }
    }
}
Listing 10-15: Mengimplementasikan method pada tipe generik secara bersyarat bergantung pada trait bounds

Kita juga bisa mengimplementasikan secara bersyarat sebuah trait buat tipe apa pun yang mengimplementasikan trait lain. Implementasi sebuah trait pada tipe apa pun yang memenuhi trait bounds-nya disebut sebagai blanket implementations (implementasi selimut) dan ini sangat banyak dipakai di standard library Rust. Misalnya, standard library mengimplementasikan trait ToString pada tipe apa pun yang mengimplementasikan trait Display. Blok impl di standard library keliatan mirip kayak kode ini:

impl<T: Display> ToString for T {
    // --snip--
}

Karena standard library punya blanket implementation ini, kita bisa memanggil method to_string yang didefinisikan sama trait ToString pada tipe apa pun yang mengimplementasikan trait Display. Misalnya, kita bisa mengubah integer jadi nilai String miliknya seperti ini karena integer mengimplementasikan Display:

#![allow(unused)]
fn main() {
let s = 3.to_string();
}

Blanket implementations ini biasanya muncul di dokumentasi buat suatu trait di bagian “Implementors”.

Traits dan trait bounds memungkinkan kita nulis kode yang memakai parameter tipe generik untuk mengurangi duplikasi, sekaligus ngasih tau compiler kalau kita maunya tipe generik itu punya perilaku tertentu. Compiler kemudian bakal memakai informasi trait bound tersebut buat mengecek apakah semua tipe konkret yang dipakai di kode kita sudah menyediakan perilaku yang benar. Di bahasa pemrograman yang dynamically typed (tipe dinamis), kita bakal dapat error pas runtime kalau kita memanggil method di suatu tipe yang sebenarnya tidak punya definisi method tersebut. Tapi Rust mindahin error-error ini ke fase compile time jadi kita dipaksa buat membenarkan masalah ini sebelum kode kita bahkan bisa dijalankan. Sebagai bonus, kita tidak perlu nulis kode buat ngecek perilaku saat runtime karena kita sudah mengeceknya pas compile time. Melakukan ini bakal meningkatkan performa tanpa harus mengorbankan fleksibilitas dari generik.