Fungsi di mana-mana

Parameter curried, shadowing, dan operator komposisi untuk membangun program dari fungsi kecil.

Daftar isi

Fungsi berperan sebagai blok pembangun utama F#, dan topik ini membahas cara kerjanya sebagai nilai yang dapat disimpan, diteruskan, dan disusun ulang. Pembahasan mencakup definisi fungsi, parameter curried beserta penerapan parsial, pengikatan ulang nama melalui shadowing, serta operator komposisi >> dan <<. Setelah topik ini dibaca, program kecil dapat dibangun dari fungsi-fungsi pendek yang disambungkan satu sama lain tanpa menulis ulang logikanya.

Fungsi adalah nilai biasa#

Fungsi di F# adalah nilai biasa, artinya fungsi dapat disimpan pada sebuah nama, dijadikan argumen fungsi lain, dan dikembalikan sebagai hasil sebuah fungsi. Definisinya ditulis dengan let nama parameter = ekspresi, dan pemanggilannya cukup dengan menuliskan nama fungsi diikuti argumennya tanpa tanda kurung. Sifat ini membuat pola seperti meneruskan proses sebagai data menjadi ringkas tanpa memerlukan sintaks khusus.

fsharp fungsi-nilai.fsx
// Fungsi adalah nilai biasa: dapat disimpan, dikirim, dan dikembalikan
let duaKali x = x * 2
let terapkan fungsi nilai = fungsi nilai

let daftar = [1; 2; 3; 4]
printfn "duaKali pada daftar : %A" (List.map duaKali daftar)
printfn "terapkan duaKali 5  : %d" (terapkan duaKali 5)

let buatPengali faktor = fun x -> x * faktor
let pengaliTiga = buatPengali 3
printfn "pengaliTiga daftar  : %A" (List.map pengaliTiga daftar)
text Keluaran
duaKali pada daftar : [2; 4; 6; 8]
terapkan duaKali 5  : 10
pengaliTiga daftar  : [3; 6; 9; 12]

Pada contoh di atas, terapkan menerima sebuah fungsi sebagai argumen pertamanya, kemudian menerapkannya pada argumen kedua. List.map juga menerima fungsi, yaitu duaKali, lalu menerapkannya pada setiap elemen daftar. Pada bagian akhir, buatPengali 3 menghasilkan fungsi baru bernama pengaliTiga, yang dibangun dari ekspresi lambda fun x -> x * faktor dan menyimpan nilai 3 dari pemanggilan pembentuknya. Tidak ada mekanisme istimewa di balik perilaku ini, karena fungsi adalah nilai sebagaimana angka dan teks.

Parameter curried dan penerapan parsial#

Definisi dengan lebih dari satu parameter membentuk fungsi curried, yaitu fungsi yang menerima argumen satu per satu dan mengembalikan fungsi baru setelah setiap argumen diterima. Signature tambah terbaca int -> int -> int, yang berarti fungsi dari int menuju fungsi lain dari int menuju int. Konsekuensinya, memanggil tambah 10 tanpa argumen kedua tetap sah, dan hasilnya adalah fungsi yang menunggu satu argumen lagi. Perilaku ini disebut penerapan parsial (partial application), yaitu memberikan sebagian argumen untuk membentuk fungsi khusus.

fsharp curried.fsx
// Parameter curried: argumen diterima satu per satu
let tambah a b = a + b
let tambahSepuluh = tambah 10

let lulus ambang nilai = nilai >= ambang
let lulusKkm = lulus 75

printfn "tambah 10 4     : %d" (tambah 10 4)
printfn "tambahSepuluh 4 : %d" (tambahSepuluh 4)
printfn "lulusKkm 80     : %b" (lulusKkm 80)
printfn "Nilai lulus KKM : %A" (List.filter lulusKkm [60; 75; 80; 50; 90])
text Keluaran
tambah 10 4     : 14
tambahSepuluh 4 : 14
lulusKkm 80     : true
Nilai lulus KKM : [75; 80; 90]

Contoh lulusKkm menunjukkan manfaat penerapan parsial pada kasus nyata. Standar kelulusan 75 cukup dinyatakan satu kali pada lulus 75, lalu fungsi hasilnya digunakan ulang untuk memeriksa nilai tunggal maupun menyaring seluruh daftar lewat List.filter. Demo berikut memperagakan penerapan parsial secara bertahap di browser, sehingga bentuk fungsi yang menunggu argumen berikutnya terlihat jelas.

Kode sumber demo
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="utf-8">
<title>Demo penerapan parsial</title>
<style>
  body { font-family: system-ui, sans-serif; margin: 0; padding: 14px; background: #f6f8fa; color: #1f2328; font-size: 14px; }
  h1 { font-size: 15px; margin: 0 0 6px; }
  code { background: #eaeef2; padding: 1px 5px; border-radius: 4px; }
  .kartu { background: #ffffff; border: 1px solid #d0d7de; border-radius: 8px; padding: 10px 12px; margin-top: 10px; }
  .kartu p { margin: 4px 0; }
  button { padding: 5px 12px; margin: 4px 6px 4px 0; cursor: pointer; }
  button:disabled { cursor: not-allowed; opacity: 0.5; }
  input { width: 90px; padding: 5px; }
  #status { min-height: 18px; font-weight: 600; }
</style>
</head>
<body>
<h1>Penerapan parsial pada fungsi curried</h1>
<p>Padanan F#: <code>let tambah a b = a + b</code> ditulis di JavaScript sebagai <code>tambah = a =&gt; b =&gt; a + b</code>.</p>
<div class="kartu">
  <p>Langkah 1, beri argumen pertama:</p>
  <button id="btnSepuluh" type="button">tambah 10</button>
  <button id="btnLima" type="button">tambah 5</button>
</div>
<div class="kartu">
  <p>Langkah 2, beri argumen kedua:</p>
  <input id="nilai" type="number" value="4">
  <button id="btnTerapkan" type="button" disabled>terapkan</button>
  <button id="btnUlangi" type="button">ulangi</button>
</div>
<p id="status">Fungsi tambah belum menerima argumen apa pun.</p>
<script>
  const tambah = a => b => a + b;
  let argumenPertama = null;
  const status = document.getElementById('status');
  const btnTerapkan = document.getElementById('btnTerapkan');
  const inputNilai = document.getElementById('nilai');

  function terimaPertama(a) {
    argumenPertama = a;
    status.textContent = 'tambah ' + a + ' mengembalikan fungsi baru yang masih menunggu satu argumen.';
    btnTerapkan.disabled = false;
  }
  function terapkanKedua() {
    const b = Number(inputNilai.value);
    status.textContent = 'tambah ' + argumenPertama + ' ' + b + ' bernilai ' + tambah(argumenPertama)(b) + '.';
  }
  function ulangi() {
    argumenPertama = null;
    status.textContent = 'Fungsi tambah belum menerima argumen apa pun.';
    btnTerapkan.disabled = true;
  }
  document.getElementById('btnSepuluh').addEventListener('click', () => terimaPertama(10));
  document.getElementById('btnLima').addEventListener('click', () => terimaPertama(5));
  btnTerapkan.addEventListener('click', terapkanKedua);
  document.getElementById('btnUlangi').addEventListener('click', ulangi);
</script>
</body>
</html>

Shadowing dan lingkup nama#

Shadowing adalah pengikatan ulang nama yang sama pada nilai baru di dalam lingkup yang lebih dalam, tanpa menyentuh pengikatan lama. Mekanisme ini berbeda dari mutasi, karena yang terjadi adalah penciptaan pengikatan baru yang menutupi nama lama pada lingkup tersebut. Shadowing terjadi pada badan fungsi, ekspresi let bersarang, dan blok kode yang berada lebih dalam dari pengikatan semula.

fsharp shadowing.fsx
// Shadowing: nama terikat ulang di lingkup dalam, nilai lama tetap utuh
let jumlah = 10

let hitungUlang () =
    let jumlah = 0
    printfn "Jumlah di dalam fungsi : %d" jumlah

hitungUlang ()
printfn "Jumlah di luar fungsi  : %d" jumlah

let rekap =
    let jumlah = jumlah + 5
    jumlah * 2
printfn "Rekap                  : %d" rekap
text Keluaran
Jumlah di dalam fungsi : 0
Jumlah di luar fungsi  : 10
Rekap                  : 30

Keluaran di atas memperlihatkan tiga kejadian sekaligus. Pengikatan jumlah bernilai 0 pada badan hitungUlang menutupi pengikatan luar hanya di dalam fungsi itu, dan nilai luar tetap 10 setelah pemanggilan selesai. Ekspresi rekap mengikat ulang jumlah menjadi 15 dengan membaca nilai lama pada ruas kanan, lalu hasil kali 30 dikembalikan tanpa mengganggu pengikatan aslinya. Parameter () pada hitungUlang menandakan fungsi yang tidak menerima masukan bermakna dan hanya menjalankan efek, sesuai konvensi F#.

Komposisi dengan » dan «#

Operator komposisi >> menyambung dua fungsi menjadi satu fungsi baru yang menjalankan keduanya secara berurutan. Ekspresi f >> g bermakna fungsi yang menerima masukan, menerapkan f, lalu meneruskan hasilnya ke g, sehingga urutan pembacaannya mengikuti urutan pengerjaannya. Operator << melakukan hal yang sama dengan urutan terbalik, yaitu fungsi di kanan dikerjakan lebih dahulu. Bentuk << jarang diperlukan, tetapi tetap tersedia apabila urutan baca yang lebih alami adalah dari kanan ke kiri, misalnya ketika fungsi yang diterapkan terakhir menjadi fokus perhatian.

fsharp komposisi.fsx
// Operator >> menyusun fungsi dari kiri ke kanan, << dari kanan ke kiri
let naikkan bobot nilai = nilai + bobot
let batasiAtas maksimum nilai = min nilai maksimum

let sesuaikan = naikkan 5 >> batasiAtas 100
let sesuaikanTerbalik = batasiAtas 100 << naikkan 5

printfn "Nilai 70 : %d" (sesuaikan 70)
printfn "Nilai 88 : %d" (sesuaikan 88)
printfn "Nilai 98 : %d" (sesuaikan 98)
printfn "Hasil << : %d dan %d" (sesuaikanTerbalik 88) (sesuaikanTerbalik 98)
text Keluaran
Nilai 70 : 75
Nilai 88 : 93
Nilai 98 : 100
Hasil << : 93 dan 100

Fungsi sesuaikan tersusun dari dua langkah kecil, yaitu menambahkan bobot 5 lalu membatasi hasil pada nilai maksimum 100. Baris terakhir membuktikan bahwa batasiAtas 100 << naikkan 5 memberikan hasil yang sama dengan bentuk >>, karena komposisi yang sama hanya ditulis dengan urutan fungsi terbalik. Komposisi menghasilkan fungsi siap pakai tanpa menuliskan parameter eksplisit, sebuah gaya yang disebut point-free.

Kesalahan umum#

Kesalahan yang sering terjadi saat mempraktikkan shadowing adalah mengikat ulang nama yang sama dua kali pada lingkup modul. Shadowing hanya berlaku pada lingkup yang lebih dalam, sehingga dua let yang berdiri sejajar pada lingkup modul diperlakukan sebagai definisi ganda dan ditolak compiler. Pola kesalahan yang sama muncul ketika definisi disalin dari contoh lain tanpa mengganti namanya, misalnya dua penghitung yang sama-sama diberi nama total. Skrip berikut memuat kesalahan tersebut.

fsharp duplicate.fsx
// tidak-dijalankan: skrip ini dieksekusi lewat perintah terminal pada blok berikutnya
// dua let dengan nama sama pada lingkup modul ditolak compiler
let jumlah = 10
let jumlah = jumlah + 5
printfn "Jumlah: %d" jumlah
bash
# menjalankan skrip yang berisi definisi ganda pada lingkup modul
# jalur berkas pada pesan error dipangkas agar barisnya ringkas
dotnet fsi duplicate.fsx 2>&1 | sed "s|.*/||"
text Keluaran
duplicate.fsx(4,5): error FS0037: Duplicate definition of value 'jumlah'

Pesan error di atas memang diharapkan, dan perbaikannya bergantung pada maksud penulisnya. Apabila nama baru memang menyimpan makna yang berbeda, ganti namanya agar keduanya tetap terbaca. Apabila pengikatan ulang diperlukan, pindahkan ke lingkup yang lebih dalam seperti badan fungsi atau ekspresi let bersarang. Bila yang dimaksud adalah perubahan nilai itu sendiri, gunakan let mutable agar operator <- sah digunakan.

Ringkasan#

  • Fungsi adalah nilai biasa yang dapat disimpan, diteruskan sebagai argumen, dan dikembalikan sebagai hasil.
  • Fungsi curried menerima argumen satu per satu, dan penerapan parsial menghasilkan fungsi baru yang siap digunakan ulang.
  • Shadowing mengikat ulang nama pada lingkup dalam tanpa mengubah nilai lama, sedangkan dua let sejajar pada lingkup modul memicu error FS0037.
  • Operator >> menyusun fungsi dari kiri ke kanan, dan << menyusunnya dari kanan ke kiri.

Referensi#