Jika bahasa pemrograman fungsional tidak dapat menyimpan status apa pun, bagaimana mereka melakukan beberapa hal sederhana seperti membaca masukan dari pengguna (maksud saya bagaimana mereka "menyimpannya"), atau menyimpan data apa pun?
Saat Anda mengumpulkannya, pemrograman fungsional tidak memiliki status — tetapi itu tidak berarti tidak dapat menyimpan data. Perbedaannya adalah jika saya menulis pernyataan (Haskell) di sepanjang baris
let x = func value 3.14 20 "random"
in ...
Saya jamin bahwa nilai dari x
selalu sama di...
: tidak ada yang bisa mengubahnya. Demikian pula, jika saya memiliki fungsi f :: String -> Integer
(fungsi yang mengambil string dan mengembalikan integer), saya yakin itu f
tidak akan mengubah argumennya, atau mengubah variabel global apa pun, atau menulis data ke file, dan seterusnya. Seperti yang dikatakan sepp2k dalam komentar di atas, non-mutabilitas ini sangat membantu untuk penalaran tentang program: Anda menulis fungsi yang melipat, memutar, dan memutilasi data Anda, mengembalikan salinan baru sehingga Anda dapat menyatukannya, dan Anda dapat yakin bahwa tidak ada dari pemanggilan fungsi tersebut dapat melakukan apa saja yang "berbahaya". Anda tahu itu x
selalu x
, dan Anda tidak perlu khawatir bahwa seseorang menulis x := foo bar
di suatu tempat di antara deklarasix
dan penggunaannya, karena itu tidak mungkin.
Sekarang, bagaimana jika saya ingin membaca masukan dari pengguna? Seperti yang dikatakan KennyTM, idenya adalah bahwa fungsi tidak murni adalah fungsi murni yang melewati seluruh dunia sebagai argumen, dan mengembalikan hasil dan dunianya. Tentu saja, Anda tidak ingin benar-benar melakukan ini: untuk satu hal, itu sangat kikuk, dan untuk hal lain, apa yang terjadi jika saya menggunakan kembali objek dunia yang sama? Jadi ini entah bagaimana menjadi abstrak. Haskell menanganinya dengan tipe IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Ini memberitahu kita bahwa main
tindakan IO yang tidak menghasilkan apa-apa; menjalankan tindakan ini adalah apa yang dimaksud dengan menjalankan program Haskell. Aturannya adalah bahwa tipe IO tidak pernah bisa lepas dari aksi IO; dalam konteks ini, kami memperkenalkan tindakan itu menggunakan do
. Jadi, getLine
mengembalikan an IO String
, yang dapat dianggap dalam dua cara: pertama, sebagai tindakan yang, ketika dijalankan, menghasilkan string; kedua, sebagai string yang "tercemar" oleh IO karena diperoleh secara tidak murni. Yang pertama lebih tepat, tetapi yang kedua bisa lebih membantu. Yang <-
mengambil String
dari ); ini semua murni (tanpa IO), jadi kami memberinya nama . Kami kemudian dapat menggunakan keduanya dan dalam . Dengan demikian, kami telah menyimpan data tidak murni (dari dalam ) dan data murni (IO String
dan menyimpannya dalam str
-tapi karena kita berada di suatu tindakan IO, kita harus membungkusnya cadangan, sehingga tidak bisa "melarikan diri". Baris berikutnya mencoba membaca integer ( reads
) dan mengambil pertandingan pertama yang berhasil (fst . head
let no = ...
no
str
...
getLine
str
let no = ...
).
Mekanisme untuk bekerja dengan IO ini sangat kuat: ini memungkinkan Anda memisahkan bagian algoritmik murni dari program Anda dari sisi interaksi pengguna yang tidak murni, dan menerapkannya pada level tipe. AndaminimumSpanningTree
Fungsi tidak mungkin mengubah sesuatu di tempat lain dalam kode Anda, atau menulis pesan kepada pengguna Anda, dan seterusnya. Itu aman.
Ini semua yang perlu Anda ketahui untuk menggunakan IO di Haskell; jika hanya itu yang Anda inginkan, Anda bisa berhenti di sini. Tetapi jika Anda ingin mengerti mengapa itu berhasil, teruslah membaca. (Dan perhatikan bahwa hal ini akan spesifik untuk Haskell — bahasa lain mungkin memilih implementasi yang berbeda.)
Jadi ini mungkin tampak seperti tipuan, entah bagaimana menambahkan ketidakmurnian pada Haskell murni. Tapi ternyata tidak — ternyata kita bisa mengimplementasikan tipe IO seluruhnya dalam Haskell murni (selama kita diberi RealWorld
). Idenya adalah ini: tindakan IO IO type
sama dengan fungsi RealWorld -> (type, RealWorld)
, yang mengambil dunia nyata dan mengembalikan baik objek bertipe type
dan yang dimodifikasi RealWorld
. Kami kemudian mendefinisikan beberapa fungsi sehingga kami dapat menggunakan tipe ini tanpa menjadi gila:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
Yang pertama memungkinkan kita untuk berbicara tentang tindakan IO yang tidak melakukan apa-apa: return 3
merupakan tindakan IO yang tidak menanyakan dunia nyata dan hanya kembali 3
. The >>=
operator, diucapkan "mengikat", memungkinkan kita untuk menjalankan tindakan IO. Ini mengekstrak nilai dari tindakan IO, meneruskannya dan dunia nyata melalui fungsi, dan mengembalikan tindakan IO yang dihasilkan. Perhatikan bahwa >>=
menegakkan aturan kami bahwa hasil tindakan IO tidak pernah diizinkan untuk lolos.
Kami kemudian dapat mengubah hal di atas main
menjadi kumpulan aplikasi fungsi biasa berikut:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
Lompatan waktu proses Haskell dimulai main
dengan yang awal RealWorld
, dan kami siap! Semuanya murni, hanya memiliki sintaksis yang mewah.
[ Sunting: Seperti yang ditunjukkan @Conal , ini sebenarnya bukan yang digunakan Haskell untuk melakukan IO. Model ini rusak jika Anda menambahkan konkurensi, atau memang cara apa pun bagi dunia untuk berubah di tengah tindakan IO, jadi Haskell tidak mungkin menggunakan model ini. Ini akurat hanya untuk komputasi sekuensial. Jadi, mungkin IO Haskell sedikit mengelak; bahkan jika tidak, itu pasti tidak se-elegan ini. Berdasarkan pengamatan @ Conal, lihat apa yang dikatakan Simon Peyton-Jones dalam Tackling the Awkward Squad [pdf] , bagian 3.1; ia menyajikan apa yang mungkin menjadi model alternatif di sepanjang garis ini, tetapi kemudian menjatuhkannya karena kerumitannya dan mengambil arah yang berbeda.]
Sekali lagi, ini menjelaskan (cukup banyak) bagaimana IO, dan mutabilitas secara umum, bekerja di Haskell; jika ini yang ingin Anda ketahui, Anda dapat berhenti membaca di sini. Jika Anda menginginkan satu dosis teori terakhir, teruslah membaca — tetapi ingat, pada titik ini, kami telah melangkah sangat jauh dari pertanyaan Anda!
Jadi satu hal lagi: ternyata struktur ini — tipe parametrik dengan return
dan >>=
- sangat umum; itu disebut monad, dando
notasi return
,, dan >>=
bekerja dengan salah satunya. Seperti yang Anda lihat di sini, monad bukanlah sihir; semua yang ajaib adalah do
blok berubah menjadi panggilan fungsi. The RealWorld
jenis adalah satu-satunya tempat kita melihat sihir apapun. Jenis seperti []
, konstruktor daftar, juga monad, dan tidak ada hubungannya dengan kode yang tidak murni.
Anda sekarang tahu (hampir) segalanya tentang konsep monad (kecuali beberapa hukum yang harus dipenuhi dan definisi matematika formal), tetapi Anda kekurangan intuisi. Ada banyak sekali tutorial monad online; Saya suka yang ini , tetapi Anda punya pilihan. Namun, ini mungkin tidak akan membantu Anda ; satu-satunya cara nyata untuk mendapatkan intuisi adalah melalui kombinasi penggunaan dan membaca beberapa tutorial pada waktu yang tepat.
Namun, Anda tidak membutuhkan intuisi itu untuk memahami IO . Memahami monad secara umum sangat penting, tetapi Anda dapat menggunakan IO sekarang. Anda bisa menggunakannya setelah saya menunjukkan main
fungsi pertama . Anda bahkan dapat memperlakukan kode IO seolah-olah itu dalam bahasa yang tidak murni! Tetapi ingatlah bahwa ada representasi fungsional yang mendasarinya: tidak ada yang curang.
(PS: Maaf tentang panjangnya. Saya pergi agak jauh.)