Parameter comptime

Menulis fungsi generik melalui parameter comptime.

Daftar isi

Fungsi biasa pada Zig menerima nilai, sedangkan fungsi dengan parameter comptime menerima informasi yang hanya diketahui saat kompilasi, misalnya sebuah tipe. Melalui mekanisme inilah Zig membangun kode generik tanpa sintaks templat terpisah. Topik ini membahas parameter comptime T: type, instansiasi yang membentuk fungsi baru untuk setiap kombinasi parameter, serta parameter comptime dengan tipe selain type. Hasil akhirnya adalah fungsi generik yang ditulis dengan bahasa Zig biasa dan dapat dijelaskan asal-usul setiap salinannya di dalam biner.

Konsep#

Parameter comptime bertipe type#

Parameter comptime dideklarasikan dengan kata kunci comptime di depan namanya. Parameter yang bertipe type menerima tipe sebagai nilai, sehingga pemanggil meneruskan i32, u8, atau tipe bentukan apa pun sebagaimana meneruskan angka. Karena nilai type hanya dapat ada pada waktu kompilasi, parameter semacam ini wajib diisi konstanta waktu kompilasi dan tidak pernah menerima hasil perhitungan runtime.

Program berikut membandingkan dua angka dengan satu fungsi maks yang sama pada tiga tipe berbeda. Tipe dioper sebagai argumen pertama, dan kedua nilai pembanding mengikuti tipe tersebut pada setiap pemanggilan.

zig maks-generik.zig
const std = @import("std");

fn maks(comptime T: type, a: T, b: T) T {
    return if (a > b) a else b;
}

pub fn main() void {
    std.debug.print("maks i32 = {d}\n", .{maks(i32, 7, 42)});
    std.debug.print("maks u8 = {d}\n", .{maks(u8, 200, 33)});
    std.debug.print("maks f64 = {d}\n", .{maks(f64, 2.5, 9.75)});
    std.debug.print("tipe hasil pertama = {s}\n", .{@typeName(@TypeOf(maks(i32, 7, 42)))});
    std.debug.print("tipe hasil kedua = {s}\n", .{@typeName(@TypeOf(maks(u8, 200, 33)))});
    std.debug.print("tipe hasil ketiga = {s}\n", .{@typeName(@TypeOf(maks(f64, 2.5, 9.75)))});
}
text Keluaran
maks i32 = 42
maks u8 = 200
maks f64 = 9.75
tipe hasil pertama = i32
tipe hasil kedua = u8
tipe hasil ketiga = f64

Pada pemanggilan maks(i32, 7, 42), argumen pertama adalah tipe i32, sehingga parameter a dan b pada instansiasi itu bertipe i32 dan hasilnya juga i32. Pemanggilan pada f64 membandingkan nilai desimal dengan aturan perbandingan yang sama. Empat baris terakhir memakai @typeName untuk menunjukkan bahwa hasil setiap pemanggilan mengikuti tipe yang diteruskan, yaitu i32, u8, dan f64, tanpa konversi terselubung di antara pemanggilan.

bash
# jalankan program dari direktori topik
zig run maks-generik.zig
text Keluaran
maks i32 = 42
maks u8 = 200
maks f64 = 9.75
tipe hasil pertama = i32
tipe hasil kedua = u8
tipe hasil ketiga = f64

Setiap instansiasi menjadi fungsi baru#

Kombinasi parameter comptime menentukan instansiasi. Kompilator menghasilkan salinan kode untuk setiap kombinasi yang dipanggil, sehingga maks(i32, ...), maks(u8, ...), dan maks(f64, ...) adalah tiga fungsi berbeda di dalam biner meskipun sumbernya ditulis satu kali. Proses ini disebut stensil (stenciling) karena kode sumber bertindak sebagai cetakan. Konsekuensinya berlaku dua arah, yaitu pemeriksaan tipe berlangsung ketat pada setiap instansiasi, sementara ukuran biner tumbuh mengikuti banyaknya kombinasi yang dipakai.

Perbandingan dengan bahasa lain membantu menempatkan mekanisme ini.

BahasaMekanisme generikSalinan kode per tipe
Zigparameter comptimeya
C++templatya
Rustmonomorfisasiya
Javapenghapusan tipe (type erasure)tidak
Gostensil dengan kamusuntuk sebagian tipe

Tabel tersebut menunjukkan dua kelompok besar. Zig, C++, dan Rust menyalin kode untuk setiap tipe, sehingga eksekusinya cepat tetapi binernya membesar. Java memilih sebaliknya, yaitu satu kode untuk semua tipe dengan penghapusan tipe, sehingga binernya kecil tetapi informasi tipe hilang saat program berjalan. Ciri khas Zig terletak pada cara menulis logika generiknya, yaitu dengan pernyataan Zig biasa di dalam fungsi, bukan dengan bahasa templat terpisah sebagaimana C++.

Contoh Lengkap#

Parameter comptime selain type#

Parameter comptime tidak terbatas pada tipe. Bilangan, comptime_int, enum, dan nilai konstanta lain juga sah, dan setiap nilai yang berbeda melahirkan instansiasi baru. Parameter semacam ini tepat digunakan apabila nilainya menentukan bentuk kode, misalnya lebar array atau jumlah iterasi yang dibuka.

Program berikut membuat eksponen menjadi parameter comptime, sedangkan bilangan dasarnya tetap parameter runtime. Fungsi yang sama melayani pemanggilan dengan base yang dihitung saat program berjalan, tetapi eksponennya harus tertulis sebagai konstanta.

zig pangkat-comptime.zig
const std = @import("std");

// eksponen diketahui saat kompilasi, sedangkan base boleh runtime
fn pangkat(base: u64, comptime eksponen: u32) u64 {
    var hasil: u64 = 1;
    var i: u32 = 0;
    while (i < eksponen) : (i += 1) {
        hasil *= base;
    }
    return hasil;
}

pub fn main() void {
    std.debug.print("10 pangkat 3 = {d}\n", .{pangkat(10, 3)});
    std.debug.print("2 pangkat 10 = {d}\n", .{pangkat(2, 10)});
    std.debug.print("7 pangkat 0 = {d}\n", .{pangkat(7, 0)});
}
text Keluaran
10 pangkat 3 = 1000
2 pangkat 10 = 1024
7 pangkat 0 = 1

Tiga pemanggilan di atas menghasilkan tiga fungsi dengan batas perulangan yang berbeda, yaitu tiga, sepuluh, dan nol iterasi. Nilai base bebas dihitung saat runtime karena parameternya bukan parameter comptime. Letak parameter comptime juga bebas diurutkan, dan pangkat menempatkan comptime eksponen setelah parameter runtime base tanpa penolakan dari kompilator. Pemisahan ini menunjukkan pembagian kerja yang disengaja, yaitu informasi yang membentuk kode disetel pada waktu kompilasi, sedangkan data yang diproses mengikuti alur program.

Kesalahan umum#

Kesalahan pada topik ini menyangkut waktu suatu nilai menjadi diketahui. Dua kasus berikut paling sering muncul.

  1. Mengoper nilai runtime sebagai argumen parameter comptime. Pemanggilan maks(x, 7, 9) dengan x bertipe i32 yang baru diisi saat berjalan ditolak dengan pesan unable to resolve comptime value beserta catatan argument to comptime parameter must be comptime-known. Berikan konstanta untuk argumen tersebut, atau ubah parameternya menjadi parameter runtime bila nilai memang baru diketahui saat program berjalan.
  2. Mengira semua parameter fungsi generik otomatis bersifat comptime. Hanya parameter yang ditandai comptime atau bertipe khusus waktu kompilasi seperti type yang menuntut konstanta. Parameter biasa tetap dievaluasi saat runtime, sehingga pemanggilan dengan variabel biasa sah dan tidak perlu dipaksakan menjadi konstanta.

Ringkasan#

  • Parameter comptime T: type menerima tipe sebagai argumen, dan parameter berikutnya yang bertipe T mengikuti tipe yang diteruskan pada setiap instansiasi.
  • Setiap kombinasi parameter comptime menghasilkan salinan fungsi baru di dalam biner, sejajar dengan templat C++ dan monomorfisasi Rust.
  • Parameter comptime juga menerima nilai selain tipe, misalnya u32 untuk eksponen yang menentukan bentuk perulangan.
  • Nilai yang baru diketahui saat runtime tidak dapat menjadi argumen parameter comptime, sehingga pembagian antara konstanta dan data harus diputuskan sejak desain.

Referensi#