Inferensi tipe

Cara compiler menyimpulkan tipe dari penggunaan tanpa anotasi eksplisit di setiap baris.

Daftar isi

Inferensi tipe (type inference) adalah kemampuan compiler menentukan tipe setiap ekspresi tanpa anotasi yang dituliskan manusia di setiap baris. Topik ini membahas sumber informasi yang digunakan compiler, mulai dari literal pada definisi, penggunaan di dalam badan fungsi, hingga generalisasi otomatis untuk fungsi yang bersifat umum. Setelah topik ini dibaca, program tetap dapat ditulis ringkas sementara pemeriksaan tipe statisnya tetap berjalan penuh, dan situasi yang menuntut anotasi eksplisit dapat dikenali.

Tipe disimpulkan dari definisi#

Sumber pertama inferensi adalah ekspresi yang berada di kanan tanda sama dengan. Literal bilangan bulat tanpa titik desimal menghasilkan int, bilangan bertitik desimal menghasilkan float, teks di antara tanda kutip menghasilkan string, dan true maupun false menghasilkan bool. Compiler mengikat nama pada tipe hasil ekspresinya secara langsung, seperti terlihat pada contoh berikut.

fsharp inferensi-dasar.fsx
// Tipe disimpulkan dari literal yang mengikat nama
let umur = 24
let beratKg = 62.5
let nama = "Citra"
let lulus = true

printfn "umur    : %d (int)" umur
printfn "beratKg : %f (float)" beratKg
printfn "nama    : %s (string)" nama
printfn "lulus   : %b (bool)" lulus
text Keluaran
umur    : 24 (int)
beratKg : 62.500000 (float)
nama    : Citra (string)
lulus   : true (bool)

Tidak satu pun baris pada contoh tersebut memuat anotasi tipe, tetapi keempat nilai tetap bertipe statis dan diperiksa ketat. Apabila umur dicoba dijumlahkan dengan teks, compiler menolaknya dengan pesan yang jelas. Literal lain mengikuti pola yang sama, misalnya akhiran m pada 15000.5m yang membentuk nilai decimal untuk kebutuhan perhitungan uang. Pada mode interaktif, dotnet fsi bahkan menampilkan signature setiap definisi yang dievaluasi, misalnya val umur: int untuk binding pertama pada contoh di atas, sehingga tipe yang tersimpan dapat diperiksa kapan pun.

Tipe disimpulkan dari penggunaan#

Sumber kedua inferensi adalah penggunaan nilai di dalam badan fungsi. Fungsi sapa pada contoh berikut tidak menyebut tipe apa pun, tetapi baris printfn "Halo, %s" nama menuntut argumen bertipe string karena penentu format %s hanya menerima string. Tipe parameter nama dengan demikian terikat menjadi string, dan signature lengkapnya terbaca sebagai val sapa: nama: string -> unit.

fsharp tipe-salah.fsx
// tidak-dijalankan: skrip ini dieksekusi lewat perintah terminal pada blok berikutnya
// printfn "%s" menuntut string, sehingga tipe parameter sapa terikat
let sapa nama =
    printfn "Halo, %s" nama

sapa "Budi"
sapa 24
bash
# menjalankan skrip yang berisi pemanggilan dengan tipe yang salah
# jalur berkas pada pesan error dipangkas agar barisnya ringkas
dotnet fsi tipe-salah.fsx 2>&1 | sed "s|.*/||"
text Keluaran
tipe-salah.fsx(7,6): error FS0001: This expression was expected to have type
    'string'
but here has type
    'int'

Pesan error di atas memang diharapkan, dan isi pesannya memperlihatkan hasil inferensi secara langsung. Compiler menyatakan bahwa ekspresi pada pemanggilan sapa 24 seharusnya bertipe string, informasi yang tidak pernah dituliskan di kode mana pun. Skrip ditolak sebelum dieksekusi, sehingga sapaan untuk Budi tidak ikut tercetak. Mekanisme yang sama berlaku pada koleksi, misalnya daftar [1; 2; 3] tersimpul sebagai int list, dan menyambungkannya dengan daftar teks memicu error FS0001 serupa.

Inferensi berjalan satu kali dari atas ke bawah, sehingga urutan penggunaan ikut menentukan hasilnya. Operator aritmetika tanpa petunjuk lain ditetapkan sebagai int, sehingga fungsi seperti let kali a b = a * b terikat pada int sejak definisinya. Pemanggilan kali 2.5 4.0 sesudahnya ditolak compiler dengan error FS0001, karena tipe parameter kali sudah terkunci pada penggunaan pertama. Kepastian urutan semacam ini membuat hasil inferensi mudah diprediksi, karena setiap keputusan tipe dapat ditelusuri kembali ke baris yang memicunya.

Generalisasi otomatis#

Fungsi yang tidak menyentuh tipe tertentu disimpulkan sebagai fungsi generik secara otomatis. let identitas x = x tidak memberi petunjuk tipe apa pun, sehingga compiler menggeneralisasinya menjadi val identitas: x: 'a -> 'a, dengan 'a sebagai variabel tipe yang menampung tipe apa pun. Generalisasi (generalization) membuat satu definisi digunakan untuk banyak tipe tanpa duplikasi maupun templat.

fsharp generalisasi.fsx
// Fungsi yang tidak menyentuh tipe tertentu digeneralisasi otomatis
let identitas x = x
let kemas x = [x]

printfn "identitas 42     : %d" (identitas 42)
printfn "identitas \"teks\" : %s" (identitas "teks")
printfn "kemas 7          : %A" (kemas 7)
printfn "kemas \"Budi\"     : %A" (kemas "Budi")
text Keluaran
identitas 42     : 42
identitas "teks" : teks
kemas 7          : [7]
kemas "Budi"     : ["Budi"]

Fungsi identitas menerima bilangan maupun teks dari definisi yang sama, sedangkan kemas membungkus masukan apa pun ke dalam daftar satu elemen. Pada contoh di atas, kemas 7 membentuk int list, sedangkan kemas "Budi" membentuk string list dari definisi yang sama. Keduanya tetap diperiksa ketat, karena sekali dipanggil dengan tipe tertentu pada satu penggunaan, compiler menuntut konsistensi tipe pada penggunaan lain yang berhubungan. Generalisasi berhenti otomatis pada batas yang memerlukan tipe konkret, misalnya begitu operasi aritmetika menyentuh parameternya. Perilaku tersebut terlihat pada fungsi pengali berikut, karena * tanpa petunjuk ditetapkan sebagai int dan pemanggilannya dengan float ditolak compiler.

fsharp Contoh: aritmetika menghentikan generalisasi
let kali a b = a * b
printfn "%d" (kali 6 7)
text Keluaran
42

Fungsi kali di atas bekerja untuk bilangan bulat dan tidak dapat digunakan untuk float tanpa anotasi maupun fungsi pendamping. Generalisasi penuh tetap menjadi milik fungsi yang benar-benar netral tipe seperti identitas dan kemas.

Anotasi tipe eksplisit#

Anotasi tipe (type annotation) dituliskan dengan titik dua setelah nama atau parameter. Anotasi diperlukan pada situasi yang tipenya tidak dapat disimpulkan atau kontrak yang ingin ditegaskan. Tiga situasi yang paling sering muncul adalah operasi aritmetika bertipe selain int, koleksi kosong yang tidak memiliki elemen rujukan, serta antarmuka fungsi yang perlu didokumentasikan pada batas modul.

fsharp anotasi-tipe.fsx
// Anotasi tipe menegaskan kontrak pada batas yang penting
let hitungPajak (tarif: decimal) (dasar: decimal) : decimal =
    dasar * tarif

let ambangLulus: float = 75.0
let kosong: int list = []

printfn "Pajak 11 persen dari 500000 : %M" (hitungPajak 0.11m 500000m)
printfn "Ambang lulus                : %f" ambangLulus
printfn "List kosong bertipe         : %A" kosong
text Keluaran
Pajak 11 persen dari 500000 : 55000.00
Ambang lulus                : 75.000000
List kosong bertipe         : []

Tanpa anotasi, operator * pada hitungPajak akan disimpulkan sebagai operasi int, dan pemanggilan dengan nilai decimal seperti 0.11m ditolak compiler. Anotasi decimal memindahkan keputusan itu ke definisi, sehingga pemanggil menjadi pihak yang menyesuaikan diri. Anotasi int list pada kosong bersifat wajib karena daftar kosong tidak membawa elemen yang dapat disimpulkan. Adapun ambangLulus tetap sah tanpa anotasi, dan anotasinya di sini berfungsi sebagai dokumentasi kontrak.

Ringkasan#

  • Tipe disimpulkan dari literal pada definisi dan dari penggunaan di dalam badan fungsi, termasuk penentu format seperti %s yang memaksa string.
  • Pesan error seperti FS0001 memperlihatkan tipe hasil inferensi secara eksplisit.
  • Fungsi yang bersifat umum digeneralisasi otomatis menjadi fungsi generik bertanda 'a.
  • Anotasi tipe digunakan untuk aritmetika non-int, koleksi kosong, dan kontrak antarmuka, tanpa mengubah mekanisme inferensi di dalam badan fungsi.

Referensi#