Pada chapter Higher-Order Functions, pabrik formatter kita anehnya “mengingat” konfigurasinya bahkan setelah pemanggilan buatFormatterHarga() selesai. Kekuatan itu bernama closure: fungsi yang membawa serta lingkungan (environment) tempat fungsi itu dibuat. Mekanika lengkap anonymous function dan use() — mulai dari sintaks dasar hingga perbedaannya dengan arrow function — sudah kita bedah pada Chapter Closures di Section Fungsi. Di sini sudut pandangnya berbeda: closure sebagai nilai first-class yang menopang pola-pola fungsional — pabrik fungsi murni, memoisasi, dan decorator.

Closure: Fungsi yang Membawa Lingkungan #

Definisi ringkasnya: closure adalah fungsi ditambah nilai-nilai yang tertangkap dari lingkungan sekitarnya saat fungsi itu dibuat. Dalam PHP, penangkapan nilai dilakukan lewat klausul use():

php
<?php

$tarifPajak = 0.11;

// Closure: fungsi + lingkungan tempat fungsi itu dibuat.
$hitungPajak = function (float $harga) use ($tarifPajak): float {
    return $harga * $tarifPajak;
};

echo $hitungPajak(100000) . "\n";

// Padanan tanpa lingkungan tertangkap: tarif menjadi parameter eksplisit.
function hitungPajakDenganTarif(float $harga, float $tarif): float
{
    return $harga * $tarif;
}

echo hitungPajakDenganTarif(100000, 0.11) . "\n";

Output yang Dihasilkan:

text
11000
11000

Kedua pendekatan menghasilkan angka yang sama, tetapi wujudnya berbeda. Closure $hitungPajak hanya butuh satu argumen karena tarif sudah ikut terbawa — variabel $tarifPajak dari scope luar menjadi bagian dari isi closure. Fungsi hitungPajakDenganTarif() membawa tarif sebagai parameter eksplisit sehingga lebih transparan, namun setiap pemanggil harus mengingat dan mengirimkannya ulang. Dari sudut pandang fungsional, closure yang menangkap nilai tetap (seperti konstanta konfigurasi) tetaplah fungsi murni: masukan sama tetap menghasilkan keluaran sama, dan tidak ada efek samping.

use() by Value vs by Reference #

use() punya dua mode yang perilakunya sangat berbeda: by value (bawaan) menangkap salinan, sedangkan by reference (dengan &) menangkap tautan ke variabel aslinya. Perbedaan ini baru terasa ketika variabel luarnya berubah setelah closure dibuat:

php
<?php

$tarifPajak = 0.11;

// use() by value: closure menyimpan salinan nilai saat dibuat.
$versiBeku = function (float $harga) use ($tarifPajak): float {
    return $harga * $tarifPajak;
};

// use() by reference: closure selalu melihat nilai terbaru variabel luar.
$versiHidup = function (float $harga) use (&$tarifPajak): float {
    return $harga * $tarifPajak;
};

$tarifPajak = 0.5;

echo $versiBeku(100000) . "\n";
echo $versiHidup(100000) . "\n";

Output yang Dihasilkan:

text
11000
50000

Mari kita telaah dua angka tersebut:

  1. $versiBeku menghasilkan 11000 karena closure menyimpan salinan 0.11 pada saat pembuatan — perubahan $tarifPajak setelahnya tidak berpengaruh.
  2. $versiHidup menghasilkan 50000 karena closure membaca variabel yang sama dengan kode luar, yang kini bernilai 0.5.

Bandingkan dua gaya berikut pada masalah yang sama, menjumlah total belanja:

php
<?php

$totalBelanja = 0;

// Anti-pola: closure memodifikasi status bersama lewat referensi.
$tambahBelanja = function (int $harga) use (&$totalBelanja): void {
    $totalBelanja += $harga;
};

$tambahBelanja(50000);
$tambahBelanja(25000);
echo "Total lewat mutasi: $totalBelanja\n";

// Alternatif fungsional: data menjadi sumber kebenaran.
$riwayatBelanja = [50000, 25000];
echo "Total lewat array_sum: " . array_sum($riwayatBelanja) . "\n";

Output yang Dihasilkan:

text
Total lewat mutasi: 75000
Total lewat array_sum: 75000

Versi pertama menyembunyikan akumulasi di dalam closure: membaca $totalBelanja tidak lagi cukup untuk memahami isinya, kita harus menelusuri seluruh pemanggilan $tambahBelanja(). Versi kedua menyimpan riwayat sebagai data lalu menghitung totalnya dengan array_sum() — total selalu bisa direkonstruksi dari sumbernya. Keduanya sah secara sintaksis; hanya versi kedua yang sejalan dengan disiplin fungsional.

Memoisasi: Cache untuk Pure Function #

Pada chapter Pure Functions kita menyebut salah satu manfaat kemurnian: hasil pemanggilan lama boleh di-cache karena masukan yang sama pasti menghasilkan keluaran yang sama. Closure adalah alat yang tepat untuk membangun cache tersebut:

php
<?php

function memoize(callable $fn): callable
{
    return function (mixed $masukan) use ($fn): mixed {
        static $cache = [];
        $kunci = serialize($masukan);
        if (!array_key_exists($kunci, $cache)) {
            $cache[$kunci] = $fn($masukan);
        }
        return $cache[$kunci];
    };
}

$jumlahPanggilanAsli = 0;

$hitungPangkatDua = function (int $n) use (&$jumlahPanggilanAsli): int {
    $jumlahPanggilanAsli++;
    return $n * $n;
};

$hitungPangkatDuaCepat = memoize($hitungPangkatDua);

echo $hitungPangkatDuaCepat(12) . "\n";
echo $hitungPangkatDuaCepat(12) . "\n";
echo $hitungPangkatDuaCepat(12) . "\n";
echo "Fungsi asli benar-benar dipanggil: $jumlahPanggilanAsli kali\n";

Output yang Dihasilkan:

text
144
144
144
Fungsi asli benar-benar dipanggil: 1 kali

Mari kita bedah mesinnya:

  1. memoize() menerima fungsi apa pun lalu mengembalikan closure baru yang berperilaku sama, tetapi dengan memori — inilah higher-order function dari chapter sebelumnya yang kini memakai closure penuh.
  2. static $cache = [] membuat cache hidup selama closure tersebut hidup; variabel static di dalam closure terikat pada satu instance closure, bukan dibagikan ke seluruh program.
  3. serialize($masukan) mengubah masukan menjadi kunci string yang aman dipakai sebagai indeks array — angka, string, bahkan array tetap menghasilkan kunci yang konsisten.
  4. $jumlahPanggilanAsli menghitung betulan berapa kali fungsi mahal asli dieksekusi; hasil 1 kali membuktikan tiga pemanggilan 144 hanya mengeksekusi perkalian sekali.

Catatan jujurnya: closure hasil memoize() sebenarnya impure — static $cache adalah status tersembunyi yang berubah antar pemanggilan. Namun impurity itu dikurung dalam satu fungsi kecil yang terdokumentasi, sementara kontrak luarnya tetap bersih: masukan sama, keluaran sama. Inilah contoh tradeoff pragmatis yang wajar dalam kode fungsional PHP.

Pola Decorate #

Pola terakhir menampakkan kekuatan closure sebagai bahan bangunan: decorator — fungsi yang menerima fungsi lain lalu mengembalikan versi dengan perilaku tambahan:

php
<?php

$labelBiasa = fn (string $teks): string => "[$teks]";

// Decorator: menerima fungsi, mengembalikan versi berperilaku tambahan.
function tambahTandaSeru(callable $fn): callable
{
    return fn (string $teks): string => $fn($teks) . '!';
}

function jadikanKapital(callable $fn): callable
{
    return fn (string $teks): string => strtoupper($fn($teks));
}

$labelRamai = jadikanKapital(tambahTandaSeru($labelBiasa));

echo $labelBiasa('diskon') . "\n";
echo $labelRamai('diskon') . "\n";

Output yang Dihasilkan:

text
[diskon]
[DISKON]!

Fungsi $labelBiasa menghasilkan [diskon]. Membungkusnya dengan tambahTandaSeru() menghasilkan fungsi yang menambahkan ! setelah kerja fungsi asli, lalu membungkus hasilnya lagi dengan jadikanKapital() menghasilkan versi kapital — urutan pembungkusan inilah yang menentukan urutan efek. Perhatikan bahwa kedua decorator tidak mengubah $labelBiasa; keduanya melahirkan fungsi baru, konsisten dengan prinsip immutability. Ketika pembungkusan seperti ini dirangkai menjadi banyak lapis, kita berada tepat di ambang function composition — tetapi sebelum ke sana, ada trio yang lebih dulu menyapa kita setiap hari: array_map(), array_filter(), dan array_reduce(), bintang chapter berikutnya.

Ringkasan #

Closure adalah fungsi plus lingkungan tertangkapnya; use() by value membekukan salinan nilai, sedangkan use (&$...) berbagi variabel hidup — yang kedua sebaiknya dihindari dalam gaya fungsional karena berarti mutasi status bersama. Closure memungkinkan pabrik fungsi murni, memoisasi (dengan catatan jujur bahwa cache-nya adalah impurity terkurung), dan decorator yang membungkus fungsi tanpa mengubahnya. Pada chapter berikutnya kita bertemu closure di habitat paling besarnya: pipeline map-filter-reduce.

Referensi #