Kombinator IO menyusun banyak aksi menjadi satu aksi tanpa menuliskan baris demi baris dalam blok do. Topik ini membahas sequence untuk menjalankan list aksi, mapM dan mapM_ untuk menerapkan satu aksi pada seluruh elemen list, serta forM dan traverse sebagai varian yang urutan argumennya lebih nyaman dibaca pada situasi tertentu. Setelah membacanya, koleksi aksi dapat dirangkai secara sistematis dan hasilnya dikumpulkan kembali sebagai list.
List aksi hanyalah nilai#
Sebuah aksi IO adalah nilai biasa, sehingga aksi boleh disimpan dalam list, diteruskan sebagai argumen, dan dikembalikan dari fungsi. List bertipe [IO ()] tidak menjalankan apa pun ketika dibuat, sebagaimana list angka tidak menghitung apa pun. Yang mengubah kumpulan deskripsi itu menjadi efek nyata adalah kombinator sequence, yang menerima list aksi dan menghasilkan satu aksi yang menjalankan semuanya berurutan.
main :: IO ()
main = do
let daftarAksi = [putStrLn "Senin: Citra", putStrLn "Selasa: Dewi"]
sequence daftarAksi
putStrLn "Jadwal tercetak"Senin: Citra
Selasa: Dewi
Jadwal tercetakBaris sequence daftarAksi menjalankan kedua pencetakan sesuai urutan elemennya, kemudian baris terakhir berjalan setelah sequence selesai. Hasil sequence adalah list berisi hasil setiap aksi, dalam hal ini [()], dan hasil itu boleh dibuang apabila tidak diperlukan. Perilaku berurutan ini berlaku untuk list aksi apa pun, sehingga urutan eksekusi dapat disusun sebagai data terlebih dahulu dan dijalankan belakangan.
Karena aksinya nilai, list aksi pun dapat dibangun dengan fungsi list biasa, misalnya map putStrLn daftarNama menghasilkan [IO ()] tanpa satu pun efek yang terjadi pada pembangunannya. Baris sequence (map putStrLn daftarNama) kemudian menjalankan semuanya, persis apa yang dilakukan mapM putStrLn daftarNama dalam satu langkah. Pemilihan antara kedua bentuk tersebut menyangkut keterbacaan, yaitu bentuk terpisah memisahkan penyusunan data dari pelaksanaannya, sedangkan mapM meringkas keduanya ketika penyusunan dan pelaksanaan terjadi pada satu tempat.
Menerapkan satu aksi ke seluruh list#
mapM dan mapM_ menerapkan satu aksi pada setiap elemen list, sebagaimana map menerapkan fungsi murni. Keduanya berbeda hanya pada hasilnya, yaitu mapM mengumpulkan hasil setiap aksi menjadi list, sedangkan mapM_ membuangnya karena tidak digunakan. Konvensi penamaan dengan garis bawah di ujung ini berlaku luas pada pustaka standar untuk varian yang mengabaikan hasil.
belanja :: [String]
belanja = ["beras", "minyak", "telur"]
main :: IO ()
main = do
mapM_ putStrLn belanja
putStrLn "-- akhir daftar --"beras
minyak
telur
-- akhir daftar --Apabila setiap aksi menghasilkan nilai yang perlu dikumpulkan, mapM menyediakannya dalam satu list. Program berikut mencetak setiap nama lalu mengumpulkan panjangnya untuk dijumlahkan.
catat :: String -> IO Int
catat kata = do
putStrLn kata
pure (length kata)
main :: IO ()
main = do
panjang <- mapM catat ["Ali", "Budi", "Citra"]
print (sum panjang)Ali
Budi
Citra
12Fungsi catat bertipe String -> IO Int, yaitu aksi yang mencetak satu kata dan menghasilkan panjangnya, sedangkan mapM catat menerapkannya pada seluruh elemen sekaligus. Hasil akhirnya bertipe IO [Int], dan setelah diikat dengan <-, list tersebut dijumlahkan dengan sum seperti list biasa. Dengan pola ini, satu aksi dapat digunakan ulang untuk satu elemen maupun seluruh list tanpa penulisan ulang.
forM dan traverse#
forM setara dengan mapM yang urutan argumennya ditukar, sehingga bentuknya forM daftar (\nama -> aksi) dan list ditulis lebih dahulu diikuti aksinya. Pola ini nyaman ketika aksinya panjang, karena pembacaan bergerak dari data menuju tindakannya. Dengan pola yang sama, traverse adalah bentuk umum mapM yang tidak mengasumsikan strukturnya list, dan untuk list keduanya identik.
import Control.Monad (forM_)
bacaSensor :: String -> IO Int
bacaSensor nama = do
let isi = "pengukuran " ++ nama
writeFile (nama ++ ".txt") isi
teks <- readFile (nama ++ ".txt")
pure (length teks)
main :: IO ()
main = do
hasil <- traverse bacaSensor ["suhu", "lembap"]
print hasil
forM_ ["suhu", "lembap"] $ \nama ->
putStrLn ("berkas " ++ nama ++ ".txt selesai diproses")[15,17]
berkas suhu.txt selesai diproses
berkas lembap.txt selesai diprosesFungsi bacaSensor menulis satu berkas, membacanya kembali, lalu menghasilkan panjang isinya, sehingga traverse bacaSensor ["suhu", "lembap"] menjalankan keduanya dan mengumpulkan hasilnya menjadi [15,17]. Baris forM_ kemudian menuliskan laporan per nama tanpa mengumpulkan hasil apa pun. Modul Control.Monad diimpor di awal berkas karena forM_ tidak diekspor oleh Prelude, sedangkan traverse sudah tersedia tanpa impor.
Membaca tipe kombinator#
Tipe mapM dan sequence pada dokumentasi memuat dua batasan kelas, yaitu Traversable untuk struktur datanya dan Monad untuk aksinya. Tipe-tipe itu dapat ditelusuri langsung melalui GHCi sebagaimana tipe aksi pada topik Tipe IO.
# telusuri tipe kombinator penerap satu aksi
ghci -e ':t mapM'
# telusuri tipe kombinator perangkai list aksi
ghci -e ':t sequence'mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)
sequence :: (Traversable t, Monad m) => t (m a) -> m (t a)Huruf t pada tipe tersebut bukan selalu list, melainkan sebarang struktur yang menjadi instans Traversable, sedangkan m bukan selalu IO, melainkan sebarang tipe yang menjadi instans Monad. Ketika t diisi list dan m diisi IO, tipe sequence menjadi [IO a] -> IO [a], persis seperti penggunaan pada topik ini. Tipe forM dan traverse mengikuti pola yang sama, hanya dengan urutan argumen dan kelas Applicative yang berbeda.
Pembacaan seperti ini menjelaskan mengapa satu fungsi bekerja untuk banyak bentuk data tanpa ditulis ulang. Generalisasi mapM dan sequence menuju kelas Traversable dan Monad adalah contoh pola yang melampaui IO itu sendiri, sehingga kebiasaan membaca tipe kombinator tetap berguna ketika struktur yang disusun bukan lagi list.
Ringkasan#
- Aksi
IOadalah nilai biasa yang dapat dikumpulkan dalam list dan dijalankan berurutan melaluisequence. mapMmenerapkan satu aksi pada seluruh elemen list dan mengumpulkan hasilnya, sedangkanmapM_membuang hasil tersebut.forMsama denganmapMdengan argumen ditukar, dantraverseadalah bentuk umumnya yang identik pada list.- Tipe kombinator menggunakan kelas
TraversabledanMonad, sehingga fungsinya bekerja untuk struktur dan tipe aksi lain, bukan hanya list danIO.
Referensi#
- Control.Monad — Hackage
— Dokumentasi modul yang memuat
forM,forM_, dan kombinator pustaka standar lainnya. - System.IO — Hackage — Dokumentasi aksi masukan keluaran yang disusun oleh kombinator pada topik ini.
- base package — Hackage
— Dokumentasi pustaka standar tempat kelas
TraversabledanApplicativedidefinisikan.