Trait dan conformance

Mendeklarasikan trait dan menuliskan conformance bergaya Stringable pada struct.

Daftar isi

Trait adalah kontrak metode antara sebuah tipe dan kode yang menggunakannya. Topik ini membahas pendeklarasian trait, penulisan conformance pada struct, pemanggilan lewat fungsi generik dengan batas trait, serta conformance untuk pencetakan teks. Setelah topik ini dibaca, struct dapat memenuhi sebuah kontrak trait dan dicetak melalui print dengan benar.

Trait sebagai kontrak metode#

Trait mendeklarasikan kemampuan yang wajib dimiliki sebuah tipe tanpa menetapkan cara implementasinya. Deklarasinya memakai kata kunci trait, diikuti nama trait dan daftar persyaratannya. Metode wajib (required method) ditulis dengan isi berupa tiga titik, yaitu ..., dan bukan pass. Bentuk tiga titik menyatakan bahwa metode tersebut hanya sebuah persyaratan, sehingga setiap tipe yang memenuhi kontrak wajib menyediakan implementasinya sendiri.

mojo Contoh: deklarasi trait dengan satu metode wajib
# tidak-dijalankan
trait Peserta:
    def nama(self) -> String: ...

Selain metode wajib, sebuah trait dapat menyediakan implementasi bawaan untuk metode tertentu. Implementasi bawaan ditulis dengan body biasa dan diwarisi oleh setiap tipe yang memenuhi kontrak, dan tetap terbuka untuk dioverride. Compiler memverifikasi seluruh persyaratan pada waktu kompilasi, sehingga kode yang mengandalkan sebuah trait dapat memanggil metode-metodenya tanpa pemeriksaan tambahan saat program berjalan.

Conformance bersifat eksplisit dan utuh. Struct yang kebetulan memiliki metode dengan nama dan tanda tangan yang sama tidak dianggap memenuhi kontrak selama trait tersebut tidak dideklarasikan pada struct-nya. Sebaliknya, struct yang mendeklarasikan sebuah trait wajib memenuhi seluruh persyaratannya, karena tidak ada conformance parsial.

Conformance, yaitu pernyataan bahwa sebuah tipe memenuhi kontrak trait, dideklarasikan dengan menuliskan nama trait di dalam tanda kurung setelah nama struct. Setelah itu, seluruh metode wajib dituliskan di dalam body struct. Compiler memeriksa kesesuaian ini pada waktu kompilasi dan menolak struct yang persyaratannya belum terpenuhi, misalnya dengan pesan yang menyatakan bahwa Petugas tidak mengimplementasikan seluruh persyaratan Peserta. Pesan tersebut disertai catatan berisi nama metode wajib yang belum diimplementasikan, sehingga bagian yang belum terpenuhi langsung terlihat.

Conformance hanya dapat dideklarasikan pada saat tipe didefinisikan. Trait tidak dapat ditambahkan belakangan ke tipe lain yang tidak didefinisikan sendiri, misalnya ke Int atau String, karena deklarasi conformance adalah bagian dari definisi tipe tersebut. Untuk tipe bawaan, dukungan trait yang tersedia sudah ditetapkan oleh pustaka standar.

Satu struct dapat memenuhi lebih dari satu trait sekaligus dengan menuliskan beberapa nama trait yang dipisahkan koma, misalnya struct Suhu(Writable, Copyable). Daftar conformance inilah yang menghubungkan kemampuan umum seperti penyalinan dan pencetakan dengan struct yang didefinisikan sendiri.

Fungsi generik dengan batas trait#

Fungsi generik (generic function) mendeklarasikan parameter compile-time yang dibatasi oleh trait, sehingga satu fungsi melayani semua tipe yang memenuhi kontrak yang sama. Bentuk penulisannya adalah def sebut[T: Peserta](p: T), dengan T sebagai parameter tipe yang dibatasi trait Peserta. Di dalam fungsi, compiler mengetahui bahwa setiap pemanggilan p.nama() sah, karena seluruh tipe yang mengisi T wajib menyediakan metode tersebut. Pemanggilan fungsinya ditulis tanpa kurung siku, karena T diinferensikan dari argumen.

Bila nama tipe yang konkret tidak diperlukan di dalam fungsi, parameter dapat ditulis langsung dengan singkatan Some[Peserta] tanpa memberi nama pada parameter tipe. Dispatch bersifat statis, yaitu pemanggilan metode ditetapkan pada waktu kompilasi, sehingga polimorfisme lewat trait tidak menambah biaya saat program berjalan.

Program berikut mendefinisikan trait Peserta, dua struct yang memenuhi kontraknya, dan satu fungsi generik yang melayani keduanya.

mojo peserta.mojo
trait Peserta:
    def nama(self) -> String: ...

struct Petugas(Peserta):
    var nama_lengkap: String
    var hari: String

    def __init__(out self, nama_lengkap: String, hari: String):
        self.nama_lengkap = nama_lengkap
        self.hari = hari

    def nama(self) -> String:
        return self.nama_lengkap

struct Siswa(Peserta):
    var nama_panggilan: String

    def __init__(out self, nama_panggilan: String):
        self.nama_panggilan = nama_panggilan

    def nama(self) -> String:
        return self.nama_panggilan

def sebut[T: Peserta](p: T) -> String:
    return "nama peserta: " + p.nama()

def main():
    var petugas = Petugas("Dewi", "Selasa")
    var siswa = Siswa("Budi")
    print(sebut(petugas))
    print(sebut(siswa))
bash
# jalankan program trait dan conformance
mojo run peserta.mojo
text Keluaran
nama peserta: Dewi
nama peserta: Budi

Dua struct dengan field yang berbeda memenuhi kontrak yang sama dan dilayani oleh satu fungsi sebut. Perluasan ke struct ketiga hanya perlu menambahkan conformance Peserta beserta implementasi nama, tanpa mengubah fungsi generiknya.

Conformance Writable untuk pencetakan#

Pencetakan nilai struct melalui print menuntut conformance pada trait Writable, yaitu trait pustaka standar untuk tipe yang dapat menuliskan dirinya sebagai teks. Trait ini diperoleh dari modul std.format dengan pernyataan from std.format import Writable. Pada dokumentasi versi sebelum 1.0, peran ini dipegang oleh trait bernama Stringable dengan metode wajib __str__, sedangkan pada Mojo 1.2.0 trait untuk pencetakan adalah Writable dengan metode write_to.

Conformance Writable dapat ditulis tanpa satu pun metode, karena @fieldwise_init sekaligus menghasilkan implementasi pencetakan berbasis refleksi. Implementasi bawaan ini mencetak nama struct beserta seluruh fieldnya, asalkan setiap field juga memenuhi Writable. Program berikut memperlihatkan perilaku bawaan tersebut beserta konversi eksplisit ke String.

mojo suhu.mojo
from std.format import Writable

@fieldwise_init
struct Suhu(Writable):
    var celsius: Float64
    var lokasi: String

def main():
    var pagi = Suhu(20.5, "kamar")
    var siang = Suhu(27.0, "teras")
    print(pagi)
    print(siang)
    print(String(pagi))
bash
# jalankan program conformance Writable
mojo run suhu.mojo
text Keluaran
Suhu(celsius=20.5, lokasi=kamar)
Suhu(celsius=27.0, lokasi=teras)
Suhu(celsius=20.5, lokasi=kamar)

Bentuk keluaran dapat diubah dengan mengoverride metode write_to pada struct. Tanda tangannya adalah def write_to(self, mut writer: Some[Writer]), dan isi field dituliskan dengan pemanggilan seperti writer.write(self.lokasi, ": ", self.celsius, " derajat"). Konversi String(s) memakai jalur yang sama dengan print, sehingga hasilnya konsisten di kedua tempat.

Kesalahan umum#

Menuliskan metode wajib trait dengan pass alih-alih tiga titik ditolak compiler. Berkas berikut memperlihatkan pesan errornya.

mojo salah-pass.mojo
trait Peserta:
    def nama(self) -> String:
        pass

def main():
    pass
bash
# jalankan berkas yang sengaja keliru
mojo run salah-pass.mojo
text Keluaran
/home/kal/my-project/content/mojo/struct-dan-trait/trait-dan-conformance/salah-pass.mojo:3:9: error: trait method with a return type must not use 'pass'; use '...' to declare the method as required
        pass
        ^
/home/kal/my-project/content/mojo/struct-dan-trait/trait-dan-conformance/salah-pass.mojo:2:9: note: in 'nama', declared here
    def nama(self) -> String:
        ^
/home/kal/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan error menetapkan aturannya secara langsung, yaitu metode trait yang memiliki tipe kembalian wajib memakai ... agar dinyatakan sebagai metode wajib. Perbaikannya adalah mengganti pass dengan ... pada deklarasi metode di dalam trait.

Kesalahan kedua adalah mencetak struct yang belum memenuhi Writable. Compiler menolak pemanggilan print dengan pesan yang menyatakan bahwa tipe struct tersebut tidak memenuhi trait Writable.

mojo salah-print.mojo
@fieldwise_init
struct Suhu:
    var celsius: Float64

def main():
    var s = Suhu(20.5)
    print(s)
bash
# jalankan berkas yang sengaja keliru
mojo run salah-print.mojo
text Keluaran
/home/kal/my-project/content/mojo/struct-dan-trait/trait-dan-conformance/salah-print.mojo:7:11: error: invalid call to 'print': an element of 'values' with type 'Suhu' does not conform to trait 'Writable'; either prove the conformance with 'conforms_to', or add conformance
    print(s)
    ~~~~~ ^
Imported from /home/kal/my-project/content/mojo/struct-dan-trait/trait-dan-conformance/salah-print.mojo:1:
Mojo/stdlib/std/io/io.mojo:359:1: note: function declared here
def print[*Ts: Writable](*values: *Ts.values, *, sep: StringSpan = " ", end: StringSpan = "\n", flush: Bool = False, var file: RawFileDescriptor = stdout)    # note - synthetic signature
^
/home/kal/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Perbaikannya adalah menambahkan Writable ke daftar conformance struct, sebagaimana dilakukan pada berkas suhu.mojo.

Kesalahan ketiga adalah mendeklarasikan conformance tanpa mengimplementasikan seluruh metode wajibnya. Compiler menolak struct tersebut dengan pesan yang menyatakan bahwa struct tidak mengimplementasikan semua persyaratan trait, disertai catatan nama metode yang tertinggal. Perbaikannya adalah menuliskan implementasi setiap metode wajib di dalam struct. Penolakan yang sama terjadi bila tanda tangan implementasi tidak cocok dengan deklarasi trait, misalnya tipe kembalian yang berbeda. Dalam kedua kasus, compiler menyebut persyaratan yang gagal, sehingga perbaikannya dapat ditulis segera.

Ringkasan#

  • Trait mendeklarasikan kontrak metode, dengan metode wajib berupa ... dan bukan pass.
  • Conformance dideklarasikan saat struct didefinisikan dengan menuliskan nama trait dalam tanda kurung, dan seluruh persyaratan wajib terpenuhi tanpa kecuali.
  • Fungsi generik dengan parameter yang dibatasi trait, misalnya def sebut[T: Peserta](p: T), melayani semua tipe yang memenuhi kontrak dengan dispatch statis.
  • Pencetakan struct pada Mojo 1.2.0 menuntut conformance Writable dari std.format, dan @fieldwise_init menyediakan implementasi pencetakan field secara otomatis.

Referensi#