Strictness

seq, bang pattern, dan data strict.

Daftar isi

Laziness memberi Haskell fleksibilitas, tetapi sebagian perhitungan tertunda perlu dihitung segera agar tidak menumpuk menjadi beban memori. Alatnya disebut strictness, yaitu kemampuan menuntut sebuah nilai dihitung pada titik yang ditentukan. Topik ini membahas tiga bentuknya, yaitu fungsi seq, bang pattern pada parameter, dan field strict pada deklarasi data, beserta tempat yang tepat untuk masing-masing.

seq dan batas WHNF#

Fungsi seq :: a -> b -> b memaksa argumen pertamanya hingga weak head normal form sebelum mengembalikan argumen keduanya. Pemaksaan berhenti pada konstruktor terluar, sehingga komponen di dalamnya tidak ikut dihitung. Program berikut menunjukkan batas itu secara langsung.

haskell paksa-whnf.hs
-- seq memaksa nilai pertamanya hingga WHNF, yaitu sampai konstruktor
-- terluarnya terlihat
main :: IO ()
main = do
  -- konstruktor pasangan sudah terlihat, sehingga undefined tidak disentuh
  print (seq (1 + 1, undefined) True)
  -- fst memaksa komponen pertamanya saja
  print (fst (1 + 1, undefined))
text Keluaran
True
2

Ekspresi seq (1 + 1, undefined) True menghasilkan True karena pemaksaan hanya menuntut konstruktor tuple terlihat, sedangkan komponen kedua yang berisi undefined tidak pernah disentuh. Baris kedua menunjukkan pemaksaan yang menembus satu langkah, yaitu fst yang meminta komponen pertama sehingga 1 + 1 dihitung menjadi 2. Penggunaan seq yang paling lazim berupa menahan akumulator agar dihitung pada setiap langkah, bentuk yang muncul pada pembahasan space leak. Bang pattern pada bagian berikutnya adalah cara menuliskan pola tersebut yang lebih rapi.

Bang pattern pada akumulator#

Bang pattern ditulis dengan tanda seru di depan parameter, dan memerlukan ekstensi BangPatterns melalui pragma di awal berkas. Parameter yang ditandai dipaksa hingga WHNF sebelum badan fungsi dijalankan, sehingga penulisan seq yang manual tidak diperlukan lagi.

haskell jumlah-bang.hs
{-# LANGUAGE BangPatterns #-}

-- bang pattern memaksa akumulator ke WHNF pada setiap langkah penggabungan
jumlah :: [Int] -> Int
jumlah = pergi 0
  where
    pergi !total [] = total
    pergi !total (x : xs) = pergi (total + x) xs

main :: IO ()
main = print (jumlah [1 .. 10])
text Keluaran
55

Parameter !total membuat setiap pemanggilan pergi menghitung akumulatornya terlebih dahulu, sehingga penjumlahan tertunda tidak pernah menumpuk pada list panjang. Tanpa tanda seru, fungsi ini menumpuk total + x sebagai thunk pada setiap langkah, kejadian yang diuraikan pada topik Space leak. Bentuk tanpa bang pattern berarti menulis let total' = total + x in total' \seq` pergi total’ xs` secara eksplisit, sehingga tanda seru hanyalah penulisan yang lebih ringkas untuk mekanisme yang sama.

foldl’ sebagai penerapan#

Fungsi foldl' dari Data.List merupakan bentuk siap pakai dari pola di atas, yaitu fold kiri yang akumulatornya dipaksa pada setiap langkah. Fungsi penghitung rata-rata berikut memakainya untuk mengalirkan list tanpa menumpuk perhitungan tertunda.

haskell rata-rata.hs
import Data.List (foldl')

-- foldl' memaksa akumulator pada setiap langkah penggabungan
rataRata :: [Int] -> Double
rataRata xs = fromIntegral (foldl' (+) 0 xs) / fromIntegral (length xs)

main :: IO ()
main = print (rataRata [2, 4, 6, 8, 10])
text Keluaran
6.0

Pemilihan foldl' pada fungsi semacam ini merupakan kebiasaan yang aman, karena jumlah elemen pada masukan tidak selalu kecil dan biaya pemaksaannya jauh lebih murah daripada biaya penumpukan thunk. Aturan praktisnya ringkas, yaitu fold kiri untuk penggabung numerik menggunakan varian dengan apostrof, sedangkan fold kanan dipertahankan untuk pemindaian yang dapat berhenti lebih awal.

Perbedaan foldl dan foldl' semata pada tanda pemaksaan itu juga menjelaskan mengapa keduanya sama-sama ada di pustaka standar. Varian lazy tetap berguna untuk penggabung yang membangun struktur, misalnya menyusun list dengan (:), karena pemaksaan pada setiap langkah justru menambah pekerjaan tanpa manfaat. Pemisahan ini membuat pilihan varian menjadi keputusan yang dapat ditebak dari bentuk penggabungnya, bukan dari ingatan terhadap kejadian yang pernah terjadi.

Field strict pada data#

Tanda seru juga berlaku pada deklarasi data, yaitu menandai field agar dipaksa pada saat konstruktor dibangun. Field strict memindahkan tanggung jawab pemaksaan dari setiap pengguna nilai menuju titik pembuatannya.

haskell data-strict.hs
-- PoinL memiliki field biasa (lazy), PoinS memiliki field pertama strict
data PoinL = PoinL Int Int
data PoinS = PoinS !Int Int
  deriving Show

skala :: Int -> PoinS -> PoinS
skala k (PoinS x y) = PoinS (x * k) (y * k)

main :: IO ()
main = do
  let p = PoinS (sum [1 .. 5]) (sum [1 .. 5])
  print (skala 2 p)
text Keluaran
PoinS 30 30

Deklarasi PoinS !Int Int menandai field pertama sebagai strict, dan anotasi tanda seru pada field data berlaku tanpa ekstensi apa pun, berbeda dengan bang pattern pada parameter yang memerlukan BangPatterns. Konstruksi PoinS (sum [1 .. 5]) (sum [1 .. 5]) menghitung penjumlahan pertama segera saat nilai dibangun, sedangkan field kedua tetap lazy dan dihitung ketika print memintanya. Field strict cocok untuk data yang fieldnya hampir selalu dibaca segera, misalnya koordinat dan hasil perhitungan kecil, sedangkan field lazy dipertahankan untuk data yang hanya dibaca pada kondisi tertentu. Penggunaan yang tepat menjadikan tanda seru bagian dari desain tipe, bukan tambalan yang diselipkan belakangan.

Field strict dan parameter strict saling melengkapi di tempat yang berbeda. Parameter strict mengatur pemaksaan pada satu pemanggilan fungsi, sehingga keputusan berada di tangan penulis fungsi, sedangkan field strict mengatur pemaksaan untuk setiap nilai tipe, keputusan yang mengikat seluruh pengguna modul. Pertanyaan desainnya mengikuti urutan yang sama, yaitu akumulator pada fungsi dikawal bang pattern, sedangkan data yang selalu dibaca penuh dikawal field strict pada deklarasinya.

Kesalahan umum#

Kesalahan pertama adalah menganggap seq menghitung nilai sampai habis. Pemaksaan seq berhenti pada konstruktor terluar, sehingga seq [1 + 1] True hanya memastikan apakah list kosong atau berisi, tanpa menghitung elemennya. Untuk pemaksaan menyeluruh, pustaka deepseq menyediakan force yang menghitung nilai sampai normal form penuh, dan fungsi itu memerlukan instans NFData.

Kesalahan kedua berupa menandai field secara sembarangan. Field strict pada struktur yang tidak pernah dibaca penuh membuat program menghitung yang tidak diperlukan, dan field strict pada tipe yang digunakan untuk stream tak hingga membuat list tak hingga gagal dibangun sama sekali, karena pembangunannya menuntut field yang tak berujung dihitung. Pemaksaan ditujukan pada akumulator dan field kecil yang pasti digunakan, bukan pada seluruh program.

Ringkasan#

  • seq memaksa nilai hingga weak head normal form, yaitu sampai konstruktor terluarnya terlihat, tanpa menyentuh komponen di dalamnya.
  • Bang pattern !total memaksa parameter pada setiap pemanggilan dan merupakan penulisan ringkas untuk seq yang manual.
  • foldl' dari Data.List adalah fold kiri dengan akumulator strict, dan menjadi pilihan baku untuk penggabungan numerik.
  • Field strict seperti PoinS !Int Int dipaksa pada saat konstruksi, sedangkan pemaksaan menyeluruh memerlukan force dari pustaka deepseq.

Referensi#