3 poin oleh GN⁺ 2025-01-20 | 2 komentar | Bagikan ke WhatsApp

Memperlakukan efek samping sebagai nilai kelas satu

  • Di Haskell, efek samping (misalnya pembuatan angka acak, output, dan sebagainya) diperlakukan seperti first class value
  • Artinya, pemanggilan fungsi yang menghasilkan efek samping seperti randomRIO(1, 6) bukan langsung nilai hasilnya, melainkan mengembalikan “objek yang mendeskripsikan aksi yang akan dijalankan nanti”
  • Objek ini akan menghasilkan nilai acak saat benar-benar dijalankan, tetapi sebelum itu hanya berisi rencana eksekusi
  • Tipe seperti IO Int merepresentasikan “aksi yang akan menghasilkan Int saat benar-benar dijalankan”, dan tidak langsung dieksekusi pada saat dipanggil, melainkan saat nanti memang diperlukan
  • Karena sifat ini, berbeda dari bahasa prosedural tradisional yang menganggap “pemanggilan fungsi = langsung dieksekusi”, di Haskell efek samping bisa dikombinasikan lalu dijalankan belakangan

Menjelaskan do block tanpa mistik

  • Blok do bukan sintaks ajaib, tetapi pada dasarnya tersusun dari dua operator: menghubungkan (bind) efek samping dan menjalankannya secara berurutan (then)

then

  • Operator *> menjalankan efek samping di kiri, membuang nilai hasilnya, lalu melanjutkan dengan efek samping di kanan
  • Misalnya putStr "hello" *> putStrLn "world" membuat satu aksi IO () yang menggabungkan dua output secara berurutan
  • Saat menulis beberapa baris di dalam blok do, secara internal digunakan operasi eksekusi berurutan seperti ini

bind

  • Operator >>= berfungsi menjalankan efek samping di kiri lalu meneruskan nilai yang dihasilkan ke fungsi di kanan
  • Contoh: randomRIO(1, 6) >>= print_side membuat efek samping yang meneruskan hasil lemparan dadu ke print_side untuk dicetak
  • Pola <- di dalam blok do adalah cara ringkas untuk mengekspresikan operator ini

Dua operator adalah seluruh isi do block

  • Pada akhirnya, blok do dibangun dari dua operator ini: *> dan >>=
  • Sintaks do sering dipakai karena keterbacaan dan kemudahannya, tetapi kelebihan Haskell lebih terasa jika kita memanfaatkan fungsi-fungsi komposisi efek samping yang lebih kaya dari itu

Fungsi yang beroperasi pada efek samping

  • Ada berbagai fungsi di pustaka standar untuk menangani efek samping dengan cara yang lebih beragam

pure

  • pure x membuat “aksi yang menghasilkan nilai x tanpa efek samping tambahan apa pun”
  • Contoh: loaded_die = pure 4 membuat IO Int yang selalu mengembalikan 4

fmap

  • Dengan bentuk fmap :: (a -> b) -> IO a -> IO b, fungsi ini membuat aksi baru yang menerapkan fungsi murni ke nilai hasil dari suatu efek samping
  • Contoh: seperti length <$> getEnv "HOME", kita bisa membuat aksi yang mengambil variabel lingkungan lalu menerapkan length untuk menghitung panjangnya

liftA2, liftA3, …

  • Fungsi seperti liftA2 dan liftA3 menggabungkan hasil dari beberapa efek samping dengan satu fungsi murni untuk membuat efek samping baru
  • Contoh: liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6)) membuat efek samping yang menjumlahkan dua nilai lemparan dadu
  • Pekerjaan yang sama juga bisa dilakukan dengan kombinasi <$> dan <*>

Intermission: apa gunanya?

  • Cara ini mungkin terlihat seperti kemampuan sederhana yang juga bisa dilakukan di bahasa lain, tetapi di Haskell ada kelebihan bahwa aksi efek samping bisa kapan saja dipisahkan ke variabel atau dikombinasikan ulang tanpa mengubah waktu eksekusi atau hasilnya
  • Dengan menangani efek samping secara terpisah, refactoring kode jadi lebih minim kebingungan, dan reuse yang aman berbasis equational reasoning menjadi mungkin

sequenceA

  • sequenceA [IO a] -> IO [a] mengubah “daftar aksi efek samping” menjadi “satu aksi efek samping tunggal yang menghasilkan daftar hasil”
  • Contoh: kita bisa mengumpulkan beberapa aksi log dalam sebuah daftar, lalu menjalankannya sekaligus nanti dengan sequenceA
  • Bahkan efek samping yang berulang tanpa batas (misalnya repeat (randomRIO(1,6))) bisa disimpan sebagai daftar, lalu hanya take n seperlunya dan dijalankan dengan sequenceA

Interlude: fungsi-fungsi praktis

  • void, sequenceA_, replicateM, replicateM_, dan lainnya berguna saat nilai hasil tidak dipakai atau saat ingin menjalankan aksi berulang kali
  • Contoh: replicateM_ 500 (putStrLn "I will not cheat again.") memungkinkan kita menjalankan efek samping berkali-kali tanpa menghitung sendiri jumlah pengulangannya

traverse

  • traverse :: (a -> IO b) -> [a] -> IO [b] membuat aksi yang menerapkan fungsi berefek samping ke setiap elemen daftar lalu mengumpulkan hasilnya sebagai daftar
  • sequenceA sebenarnya sama dengan traverse id, dan traverse_ adalah versi yang membuang hasilnya

for

  • for punya fungsi yang sama dengan traverse, tetapi menerima argumen dalam urutan terbalik

  • Contoh: bentuk for numbers $ \n -> ... memungkinkan ekspresi yang terasa alami seperti sintaks “for loop”

  • Berkat komposisi seperti ini, pengulangan, traversal, dan transformasi struktur data yang di bahasa lain sering memerlukan sintaks khusus bisa diimplementasikan di Haskell dengan kombinasi fungsi pustaka

Memanfaatkan sifat kelas satu dari efek

  • Jika efek samping dimanfaatkan secara aktif sebagai nilai kelas satu di Haskell, kita bisa mengurangi duplikasi kode dan memperbaiki struktur program
  • Misalnya, dalam logika faktorisasi prima bilangan besar dengan cache, kita bisa memakai State alih-alih IO untuk membentuk struktur yang “punya efek samping tetapi tidak memengaruhi dunia luar”
  • Efek samping yang distrukturkan seperti ini hanya diterapkan pada bagian yang perlu, sementara kode lainnya bisa tetap sebagai fungsi murni, sehingga keamanan dan fleksibilitas sama-sama terjaga
  • Pada akhirnya, dengan evalState dan sejenisnya, efek tersebut bisa dijalankan lalu hasilnya diubah menjadi nilai murni

Hal-hal yang tidak perlu Anda pedulikan

  • Berbagai nama lama dari era Haskell terdahulu (>>, return, mapM, dan sebagainya) kini bisa digantikan oleh fungsi modern (*>, pure, traverse, dan sebagainya)
  • Nama-nama itu berasal dari “nama lama atau desain yang berpusat pada monad”, sedangkan sekarang pendekatan berbasis Applicative atau Functor yang lebih umum lebih dianjurkan

Lampiran A: Menghindari kesuksesan dan ketidakbergunaan

  • Ungkapan “Haskell avoids success” berarti “bahasa ini tidak mengorbankan nilai-nilai dasarnya demi popularitas atau kemudahan semata”
  • “Haskell is useless” merujuk pada konteks bahwa pada awalnya bahasa ini hanya mengizinkan fungsi murni sepenuhnya sehingga tampak seperti bahasa yang benar-benar tidak bisa melakukan apa pun, tetapi kemudian memperoleh kepraktisan lewat cara memperlakukan efek samping sebagai sesuatu yang ‘kelas satu’

Lampiran B: Mengapa fmap memetakan baik efek samping maupun daftar

  • fmap memiliki bentuk yang sangat umum (Functor f => (a -> b) -> f a -> f b), sehingga bisa diterapkan secara seragam ke berbagai container atau tipe berefek seperti list, Maybe, dan IO
  • Jika fmap diterapkan ke list, ia menerapkan fungsi ke semua elemen; jika diterapkan ke IO, ia menerapkan fungsi ke nilai hasilnya
  • Dengan demikian, seluruh “struktur yang bisa dikenai penerapan fungsi” disebut Functor

Lampiran C: Foldable dan Traversable

  • Foldable adalah struktur yang elemennya bisa ditelusuri dan diproses
  • Traversable adalah struktur yang bukan hanya bisa ditelusuri, tetapi juga bisa dibangun ulang dengan bentuk yang sama menggunakan elemen baru
  • Agar sequenceA atau traverse bisa mengumpulkan nilai sambil mempertahankan struktur asal, struktur tersebut harus bersifat Traversable
  • Struktur data seperti tree atau Set bisa memiliki bentuk yang berubah tergantung nilainya, sehingga dibedakan antara kasus yang hanya bisa ditelusuri (Foldable) dan kasus yang benar-benar bisa dibangun ulang (Traversable)
  • Sesuai kebutuhan, efek samping juga bisa ditangani secara fleksibel lewat cara seperti mengubahnya dulu menjadi list lalu memakai traverse

2 komentar

 
bbulbum 2025-01-21

Kalau lihat-lihat Reddit, iklannya sering banget muncul.. Tapi dari namanya saja sudah terasa ada semacam hambatan psikologis.
Entah kenapa kesannya seperti bahasa yang sangat sulit dan kuat..

 
GN⁺ 2025-01-20
Opini Hacker News
  • Sistem tipe Haskell memang rumit jika dibandingkan dengan bahasa populer lain. Khususnya, operator seperti *>, <*>, dan <* meningkatkan kurva belajar di seluruh codebase

    • Jika tidak memakai Haskell selama sebulan, orang bisa perlu mempelajari lagi operator seperti >>= dan >> agar produktivitas tetap terjaga
    • Sulit mempelajari konsep Haskell sendirian tanpa berdiskusi dengan orang lain
  • Haskell membantu memperbaiki pemrograman imperatif

    • Dengan efek first-class dan pattern, boilerplate code bisa dihilangkan
    • Berkat type safety, kita bisa menulis kode yang relatif minim bug dengan cepat
  • Versi generalisasi dari traverse/mapM sangat berguna karena bekerja bukan hanya untuk list tetapi untuk semua tipe Traversable

    • Dapat digunakan dalam bentuk traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
    • Di bahasa lain, efek serupa sering kali mengharuskan banyak kode ditulis manual
  • Haskell memiliki monad yang kuat, dan ini membuat Haskell terasa lebih prosedural

    • Variabel perantara bisa digunakan di dalam blok do
  • Salah satu perangkat lunak yang ditulis dengan Haskell adalah ImplicitCAD

  • Kode Haskell bisa dibaca seperti bahasa prosedural, tetapi tetap memberi keunggulan saat bekerja dengan fungsi berefek samping

    • Bekerja dengan monad IO itu rumit, dan menjadi lebih rumit lagi saat ingin memakai tipe monad lain
  • >> adalah nama lama untuk <i>>, dan keduanya adalah operator asosiatif kiri

    • >> didefinisikan sebagai infixl 1 dan <i>> didefinisikan sebagai infixl 4, sehingga <i>> terikat lebih kuat daripada >>
  • IO a dan a di Haskell bisa terasa mirip dengan asinkron dan sinkron

    • Yang pertama mengembalikan promise/future yang harus ditunggu
  • Di bahasa lain, IO sederhana bisa dilakukan dengan fungsi seperti console.log("abc")

    • Ada pertanyaan apakah Haskell memiliki perbedaan dibanding IO semacam itu
  • Orang yang belum pernah mencoba Haskell bisa merasa bahwa Haskell nyata yang memakai ekstensi GHC terlalu rumit

    • Hal ini bisa menurunkan minat terhadap Haskell