Tipe Parametrik

Menuliskan parameter tipe pada struct dan signature fungsi agar satu definisi melayani banyak kombinasi tipe.

Daftar isi

Tipe parametrik (parametric type) adalah tipe yang membawa satu atau lebih parameter tipe, sehingga satu definisi melayani banyak kombinasi tipe tanpa menyalin kode. Notasi umumnya berupa nama tipe dengan kurung kurawal, misalnya Kotak{T}. Topik ini membahas parameter tipe pada struct, pembedaan antarinstansiasi, parameter tipe pada signature fungsi, kendala T<:Real, serta Vector{T} sebagai contoh bawaan.

Parameter tipe pada struct#

Parameter tipe ditulis dalam kurung kurawal setelah nama struct, dan huruf T menjadi simbol yang diisi pada saat instansiasi (instantiation). Julia menyimpulkan isi parameter dari argumen konstruktor, tetapi parameter juga dapat ditulis eksplisit seperti Kotak{String}("kata"). Tipe hasilnya selalu ditulis lengkap bersama parameternya, misalnya Kotak{Int64}.

Sebuah struct boleh membawa lebih dari satu parameter, misalnya struct Pasangan{A, B} yang menyimpan dua nilai dengan tipe berbeda. Aturannya sama, yaitu parameter diisi pada saat instansiasi sehingga menghasilkan Pasangan{Int64, String} dan sejenisnya. Contoh pada topik ini cukup memakai satu parameter agar notasinya tetap ringkas.

julia kotak.jl
struct Kotak{T}
    isi::T
end

a = Kotak(42)
b = Kotak(4.5)
c = Kotak("kata")
println(a)
println(b)
println(c)
println(typeof(a))
println(typeof(b))
println(typeof(c))
println(a.isi * 2)
text Keluaran
Kotak{Int64}(42)
Kotak{Float64}(4.5)
Kotak{String}("kata")
Kotak{Int64}
Kotak{Float64}
Kotak{String}
84

Satu definisi Kotak menghasilkan tiga tipe berbeda sesuai tipe argumen konstruktornya. Pada setiap instansiasi, tipe field isi diketahui secara pasti, sehingga operasi seperti a.isi * 2 dihitung dengan Int64 tanpa ketidakpastian tipe. Ketepatan inilah yang membedakan tipe parametrik dari field bertipe abstrak, karena tipe abstrak menunda penentuan tipe hingga program berjalan.

Instansiasi yang berbeda adalah tipe berbeda#

Kotak{Int64} dan Kotak{Float64} adalah dua tipe yang berbeda meskipun berasal dari satu definisi yang sama. Nama Kotak tanpa kurung kurawal menyatakan seluruh keluarga Kotak{T} sekaligus, sehingga pemeriksaan Kotak{Int64} <: Kotak bernilai true. Fungsi fieldtype membaca tipe field pada instansiasi tertentu tanpa perlu membuat objeknya. Pembedaan ini penting saat menyusun metode, karena metode yang ditulis untuk Kotak{Int64} tidak otomatis melayani Kotak{Float64}, sedangkan anotasi Kotak tanpa parameter mencakup seluruh instansiasinya.

julia Pembedaan instansiasi parametrik
struct Kotak{T}
    isi::T
end

println(Kotak{Int64} <: Kotak)
println(Kotak{Int64} == Kotak{Float64})
println(typeof(Kotak(42)) == Kotak{Int64})
println(fieldtype(Kotak{Int64}, :isi))
println(fieldtype(Kotak{Float64}, :isi))
text Keluaran
true
false
true
Int64
Float64

Hasil perbandingan memperlihatkan bahwa kedua instansiasi tidak sama, sedangkan typeof(Kotak(42)) persis bernilai Kotak{Int64}. Setiap instansiasi menghasilkan tipe konkret dengan tipe field yang pasti, sebagaimana terbaca dari fieldtype. Keberagaman ini tidak menyalin definisi, karena satu definisi Kotak tetap menjadi satu-satunya sumber kebenaran.

Parameter tipe pada signature fungsi#

Notasi where {T} memasukkan parameter tipe ke dalam signature fungsi. Metode sama_jenis(a::T, b::T) where {T} hanya cocok apabila kedua argumen bertipe sama persis, dan metode cadangan tanpa anotasi menangani kombinasi lainnya. Notasi yang sama berlaku pada metode dengan lebih banyak argumen, misalnya sama(a::T, b::T, c::T) where {T} untuk tiga argumen bertipe sama. Pola ini memisahkan kasus bertipe sama dari kasus campuran tanpa pemeriksaan manual di dalam badan fungsi.

julia Signature fungsi dengan where
sama_jenis(a::T, b::T) where {T} = true
sama_jenis(a, b) = false

println(sama_jenis(1, 2))
println(sama_jenis(1, 2.0))
println(sama_jenis("a", "b"))
println(sama_jenis('x', 'y'))
text Keluaran
true
false
true
true

Pemanggilan sama_jenis(1, 2) dan sama_jenis("a", "b") cocok dengan metode pertama karena kedua argumennya bertipe sama, yaitu Int64 dan String. Pemanggilan sama_jenis(1, 2.0) menggabungkan Int64 dengan Float64, sehingga metode T, T tidak cocok dan metode cadangan menjawab false.

Metode cadangan pada contoh memainkan peran penting. Tanpa metode tersebut, pemanggilan dengan tipe campuran memunculkan MethodError karena tidak ada metode yang cocok. Dengan cadangan itu, perilaku fungsi tetap terdefinisi untuk seluruh kombinasi, sedangkan kasus bertipe sama diperlakukan khusus oleh metode pertama.

Kendala pada parameter tipe#

Parameter tipe dapat dibatasi dengan kendala (constraint) berbentuk T<:Real. Metode dengan kendala semacam itu hanya menerima tipe yang berada di bawah Real, dan pemanggilan dengan argumen di luar kendala memunculkan MethodError. Kendala membuat maksud metode lebih jelas sekaligus memberi pesan error yang lebih informatif.

julia Kendala T <: Real pada signature
dua_kali(x::T) where {T<:Real} = 2x

println(dua_kali(21))
println(dua_kali(2.5))
println(dua_kali(1//3))
try
    dua_kali("halo")
catch e
    println(typeof(e))
    println(split(sprint(showerror, e), "\n")[1])
end
text Keluaran
42
5.0
2//3
MethodError
MethodError: no method matching dua_kali(::String)

Tipe Real mencakup Int64, Float64, dan juga Rational, sehingga tiga pemanggilan pertama dilayani satu metode dengan tipe hasil yang mengikuti argumennya. Pemanggilan dengan String tidak memenuhi kendala, dan MethodError menyatakan tidak ada metode untuk kombinasi tersebut. Tanpa kendala, metode tetap dapat ditulis untuk semua tipe, tetapi risiko dipanggil dengan tipe yang tidak bermakna menjadi lebih besar.

Kendala tidak mengubah isi perhitungan, tetapi mempersempit tipe argumen yang diterima metode. Metode tanpa kendala seperti dua_kali(x) = 2x tetap dapat ditulis untuk semua tipe, dan operasi 2x baru gagal saat dipanggil dengan tipe yang tidak mendukungnya. Kendala memindahkan kegagalan itu ke pemilihan metode, sehingga pesan errornya muncul lebih awal dan lebih jelas.

Metode untuk tipe parametrik#

Parameter tipe juga dapat dipakai di dalam badan metode. Signature tampilkan(k::Kotak{T}) where {T} menangkap parameter T dari tipe argumen, sehingga parameter itu dapat disisipkan ke pesan atau dipakai untuk perhitungan. Metode seperti ini melayani seluruh keluarga Kotak tanpa menuliskannya satu per satu.

julia Parameter tipe di dalam badan metode
struct Kotak{T}
    isi::T
end

tampilkan(k::Kotak{T}) where {T} = "Kotak berisi $T: $(k.isi)"

println(tampilkan(Kotak(42)))
println(tampilkan(Kotak(4.5)))
println(tampilkan(Kotak("kata")))
text Keluaran
Kotak berisi Int64: 42
Kotak berisi Float64: 4.5
Kotak berisi String: kata

Interpolasi $T menampilkan nama parameter tipe, sedangkan $(k.isi) menampilkan isinya. Satu metode melayani ketiga instansiasi sekaligus, dan Julia menyusun versi khusus untuk setiap T yang benar-benar dipakai. Inilah bentuk paling ringkas dari pola yang juga dipakai fungsi bawaan pada wadah Vector berikut.

Signature semacam ini menjadi kekuatan utama tipe parametrik. Metode dapat membaca parameter T untuk menyusun nilai bertipe itu tanpa mengetahui tipe konkret pada saat menulis kode, misalnya membuat objek lain dengan tipe elemen yang sama. Daftar metode sebuah fungsi pun tetap ringkas karena satu signature mencakup seluruh keluarga Kotak.

Vector sebagai tipe parametrik bawaan#

Tipe parametrik juga menjadi dasar wadah bawaan Julia. Array satu dimensi bertipe Vector{T}, dan literal [1, 2, 3] menyimpulkan parameternya sehingga bertipe Vector{Int64}. Fungsi eltype membaca tipe elemen array, sedangkan fungsi push! menambah elemen dengan tetap mematuhi tipe tersebut. Parameter tipe tidak mengikat ukuran array, sehingga array tetap bertipe sama setelah elemennya ditambahkan.

julia Vector dengan parameter tipe
v = [1, 2, 3]
println(typeof(v))
println(eltype(v))
push!(v, 4)
println(v)
w = Float64[1.5, 2.5]
println(typeof(w))
println(eltype(w))
text Keluaran
Vector{Int64}
Int64
[1, 2, 3, 4]
Vector{Float64}
Float64

Notasi Float64[1.5, 2.5] menetapkan tipe elemen secara eksplisit sebelum literal array, sehingga w bertipe Vector{Float64} sejak dibuat. Mekanisme yang sama bekerja pada Dict dan wadah lainnya, yaitu satu definisi dengan parameter tipe yang diisi sesuai kebutuhan. Apabila literal array menggabungkan dua tipe berbeda, parameternya dinaikkan ke tipe bersama, misalnya [1, 2.5] bertipe Vector{Float64}.

Perilaku pencampuran tipe ini sebaiknya diperhatikan. Literal array yang dicampur tanpa disadari menaikkan seluruh elemennya ke tipe bersama, dan perubahan tersebut dapat diperiksa dengan typeof sebelum array dipakai lebih jauh. Penetapan tipe secara eksplisit menjadi cara menghentikan kenaikan yang tidak dikehendaki.

Ringkasan#

  • Parameter tipe T menghasilkan tipe berbeda untuk setiap instansiasi, misalnya Kotak{Int64} yang berbeda dari Kotak{Float64}.
  • Notasi where {T} menuliskan parameter pada signature fungsi, dan kendala T<:Real membatasi tipe yang diterima.
  • Vector{T} adalah tipe parametrik bawaan, dengan eltype membaca tipe elemennya dan push! mematuhi tipe tersebut.

Referensi#