Aturan borrowing

Menalar konvensi bawaan yang meminjam secara immutable dan eksklusivitas peminjaman.

Daftar isi

Parameter fungsi di Mojo tidak menyalin nilai argumennya secara bawaan. Konvensi bawaannya adalah peminjaman (borrowing), yakni pembacaan nilai milik pemanggil melalui referensi yang tidak dapat diubah. Topik ini membahas konvensi tersebut beserta aturan eksklusivitasnya, sehingga pilihan antara peminjaman, penulisan lewat mut, dan penyalinan dapat ditentukan dengan sadar pada setiap signature fungsi.

Peminjaman immutable sebagai konvensi bawaan#

Parameter yang dideklarasikan tanpa konvensi apa pun, misalnya a: Int, menerima referensi ke nilai milik pemanggil. Fungsi yang menerima peminjaman boleh membaca dan menghitung dengan nilainya, tetapi tidak boleh mengubah nilai yang dipinjamnya. Tidak ada data yang ditiru pada saat pemanggilan, sehingga biaya pemanggilan tidak bertambah mengikuti besar nilai yang dipinjam. Konvensi ini menjadi pilihan yang tepat untuk fungsi pembaca, yaitu mayoritas fungsi pada kebanyakan program. Signature pun langsung menjadi dokumentasi, karena pembaca yang melihat parameter tanpa konvensi mengetahui fungsi tersebut tidak pernah menulis ke argumennya.

Jalankan program berikut yang memakai dua bentuk konvensi sekaligus.

mojo peminjaman.mojo
def jumlah(a: Int, b: Int) -> Int:
    return a + b

def baca(a: Int) -> Int:
    return a + 1

def ganti(mut a: Int):
    a = 99

def main():
    var x = 5
    print(baca(x))
    print(x)
    ganti(x)
    print(x)
    print(jumlah(x, x))
bash
# jalankan program peminjaman beserta mut dan jumlahnya
mojo run peminjaman.mojo
text Keluaran
6
5
99
198

Fungsi baca mengembalikan a + 1 tanpa menyentuh nilai aslinya, sehingga print berikutnya masih menampilkan 5. Fungsi ganti menerima peminjaman yang dapat diubah lewat mut, dan perubahan menjadi 99 langsung terlihat oleh pemanggil. Baris terakhir memanggil jumlah(x, x), yaitu mengirim variabel yang sama sebagai dua argumen yang dipinjam secara immutable. Pemanggilan seperti ini sah karena dua pembaca boleh hadir bersamaan, dan keluaran 198 menjadi buktinya, karena kedua argumen membaca nilai 99 yang sama.

Peminjaman pada struct dan wadah#

Konvensi bawaan berlaku sama untuk nilai bertipe struct maupun wadah, dan di sanalah manfaatnya paling terasa. Fungsi yang menerima parameter bawaan membaca field atau elemen milik pemanggil tanpa menyalin struct atau wadahnya, sehingga besar tipe yang dipinjam tidak menambah biaya pemanggilan. Jalankan program berikut yang menghitung kuadran sebuah titik hanya dari field-nya.

mojo pinjam-struct.mojo
struct Titik:
    var x: Int
    var y: Int

    def __init__(out self, x: Int, y: Int):
        self.x = x
        self.y = y

def kuadran(t: Titik) -> Int:
    if t.x >= 0 and t.y >= 0:
        return 1
    if t.x < 0 and t.y >= 0:
        return 2
    if t.x < 0 and t.y < 0:
        return 3
    return 4

def main():
    var t = Titik(-2, 5)
    print(kuadran(t))
    print(t.x, t.y)
bash
# jalankan program yang meminjam struct melalui parameter bawaan
mojo run pinjam-struct.mojo
text Keluaran
2
-2 5

Fungsi kuadran menerima Titik tanpa konvensi apa pun dan hanya membaca t.x serta t.y, sehingga pemanggilan kuadran(t) tidak menyalin struct itu. print kedua menampilkan -2 5, yang membuktikan nilai asli tetap utuh dan tetap terisi setelah pemanggilan selesai. Pola ini berulang pada program nyata, karena sebagian besar fungsi menerima struct atau wadah hanya untuk membacanya.

Konvensi yang sama berlaku pada wadah seperti List dan Dict. Fungsi yang meminjam wadah boleh membaca elemen, menghitung ringkasan, atau mencari isinya tanpa menyalin wadah tersebut, sehingga ukuran koleksi tidak menambah biaya pemanggilan. Aturan eksklusivitas juga dihitung pada nilai yang dipinjam, sehingga penalaran pada struct sederhana dapat diterapkan apa adanya pada koleksi besar.

Penulisan lewat peminjaman ditolak compiler#

Karena parameter bawaan adalah referensi yang tidak dapat diubah, penugasan pada parameter tersebut ditolak. Penolakan ini bukan keterbatasan sesaat, melainkan janji yang diberikan kepada pemanggil, yaitu nilai yang dipinjam tetap utuh selama pemanggilan berlangsung. Jalankan berkas berikut yang mencoba menulis lewat parameter bawaan.

mojo mutasi-gagal.mojo
def baca(a: Int):
    a = 1

def main():
    var x = 5
    baca(x)
    print(x)
bash
# jalankan berkas yang mencoba menulis lewat peminjaman immutable
mojo run mutasi-gagal.mojo 2>&1 | sed -e "s|$PWD/||" -e "s|$HOME/|~/|"
text Keluaran
mutasi-gagal.mojo:2:5: error: expression must be mutable in assignment
    a = 1
    ^
~/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan expression must be mutable in assignment menyatakan bahwa a bukan tempat penyimpanan milik fungsi, melainkan jendela baca ke nilai milik pemanggil. Apabila sebuah fungsi memang perlu mengubah nilai pemanggilnya, maksud itu dituliskan secara eksplisit pada signature lewat mut. Dengan cara ini, pembaca signature mengetahui sejak awal fungsi mana yang menulis dan fungsi mana yang hanya membaca, tanpa harus menelusuri badan fungsinya.

Penolakan yang sama menjangkau penulisan pada bagian dari nilai yang dipinjam, misalnya field struct, karena bagian itu ikut tercakup dalam janji keutuhan nilainya. Jalur penulisan yang sah selalu dinyatakan di awal, baik lewat parameter mut pada fungsi maupun mut self pada metode, sehingga sumber perubahannya selalu dapat ditemukan pada signature. Membaca signature terlebih dahulu pun menjadi kebiasaan yang berguna saat menalar program Mojo.

Eksklusivitas peminjaman#

Peminjaman immutable boleh bertumpuk pada variabel yang sama, sedangkan peminjaman yang dapat diubah wajib eksklusif. Aturan ini diperiksa compiler, dan pelanggarannya ditolak sebelum program berjalan. Program berikut mengirim variabel yang sama sebagai dua argumen mut, sehingga dua jalur penulisan mengarah ke satu nilai yang sama.

mojo bentrok-gagal.mojo
def ubah_dua(mut a: Int, mut b: Int):
    a = a + 1
    b = b + 1

def main():
    var x = 1
    ubah_dua(x, x)
    print(x)
bash
# jalankan berkas yang menabrakkan dua peminjaman mut pada satu variabel
mojo run bentrok-gagal.mojo 2>&1 | sed -e "s|$PWD/||" -e "s|$HOME/|~/|"
text Keluaran
bentrok-gagal.mojo:7:13: error: aliasing values passed mutably to 'a' argument and passed mutably to 'b' argument in 'ubah_dua' call
    ubah_dua(x, x)
            ^~  ~
bentrok-gagal.mojo:7:13: note: 'origin_of(x)' value is passed through aliasing 'mut' argument 'b'
    ubah_dua(x, x)
            ^   ~
~/.pixi/envs/mojo/bin/mojo: error: failed to parse the provided Mojo source module

Pesan error menyebut aliasing, yaitu dua jalur yang mengarah ke nilai yang sama. Apabila penulisan semacam ini diizinkan, hasil akhir nilai bergantung pada urutan evaluasi argumen, dan program menjadi sulit dinalar. Aturan eksklusivitas mencegahnya sejak kompilasi dengan syarat yang jelas, yaitu satu penulis atau banyak pembaca, dan tidak pernah keduanya bersamaan. Perbaikan untuk kasus ini pun sederhana, yaitu menyiapkan dua variabel terpisah, sehingga masing-masing argumen meminjam nilai yang berbeda.

Aturan tersebut juga melarang kombinasi sebaliknya, yaitu satu argumen menulis lewat mut sementara argumen lain meminjam nilai yang sama, karena bentuk itu memuat satu penulis dan satu pembaca sekaligus. Pesannya mudah diingat karena menyebut origin_of(x), yaitu penanda bahwa compiler melacak asal variabel, bukan sekadar namanya. Bila penulisan dan pembacaan memang keduanya dibutuhkan, selesaikan penulisan terlebih dahulu, lalu lakukan pembacaan pada pemanggilan berikutnya.

Kesalahan umum#

  • Menganggap parameter bawaan sebagai salinan. Parameter tanpa konvensi adalah peminjaman, sehingga besar nilainya tidak menambah biaya pemanggilan, tetapi fungsi tidak boleh menulis lewat jalur tersebut. Perbaikannya adalah memilih konvensi sesuai kebutuhan, yaitu bawaan untuk pembacaan, mut untuk penulisan, dan var bila fungsi membutuhkan nilai miliknya sendiri.
  • Mengirim variabel yang sama ke dua argumen mut. Compiler menolaknya dengan pesan error aliasing karena kedua argumen menulis ke nilai yang sama. Perbaikannya adalah menyalin atau memisahkan nilai tersebut ke dua variabel terlebih dahulu, sehingga tiap argumen meminjam pemilik yang berbeda.

Ringkasan#

  • Parameter tanpa konvensi meminjam secara immutable, yaitu membaca nilai pemanggil tanpa menyalin dan tanpa mengubah.
  • Penugasan pada parameter bawaan ditolak compiler dengan pesan expression must be mutable in assignment.
  • Peminjaman immutable boleh bertumpuk pada satu variabel, sedangkan penulisan lewat mut wajib eksklusif dan ditolak bila bertabrakan.

Referensi#