Apakah manfaat dari pola mono IO untuk menangani efek samping murni bersifat akademis?

17

Maaf untuk pertanyaan efek samping FP + lainnya, tetapi saya tidak dapat menemukan yang sudah ada yang menjawab ini untuk saya.

Pemahaman saya (terbatas) tentang pemrograman fungsional adalah bahwa keadaan / efek samping harus diminimalkan dan dipisahkan dari logika stateless.

Saya juga mengumpulkan pendekatan Haskell untuk ini, monad IO, mencapai ini dengan membungkus tindakan negara dalam sebuah wadah, untuk kemudian dieksekusi, dianggap di luar lingkup program itu sendiri.

Saya mencoba memahami pola ini, tetapi sebenarnya untuk menentukan apakah akan menggunakannya dalam proyek Python, jadi ingin menghindari spesifik Haskell jika poss.

Contoh kasar masuk.

Jika program saya mengonversi file XML ke file JSON:

def main():
    xml_data = read_file('input.xml')  # impure
    json_data = convert(xml_data)  # pure
    write_file('output.json', json_data) # impure

Bukankah pendekatan IO monad efektif untuk melakukan ini:

steps = list(
    read_file,
    convert,
    write_file,
)

kemudian membebaskan diri dari tanggung jawab dengan tidak benar-benar memanggil langkah-langkah itu, tetapi membiarkan penerjemah melakukannya?

Atau dengan kata lain, itu seperti menulis:

def main():  # pure
    def inner():  # impure
        xml_data = read_file('input.xml')
        json_data = convert(xml_data)
        write_file('output.json', json_data)
    return inner

kemudian mengharapkan orang lain untuk menelepon inner()dan mengatakan pekerjaan Anda selesai karena main()itu murni.

Seluruh program pada akhirnya akan terkandung dalam monad IO, pada dasarnya.

Ketika kode benar-benar dieksekusi , semuanya setelah membaca file tergantung pada status file itu sehingga masih akan menderita bug terkait negara yang sama dengan implementasi imperatif, jadi apakah Anda benar-benar mendapatkan sesuatu, sebagai programmer yang akan mempertahankan ini?

Saya benar-benar menghargai manfaat mengurangi dan mengisolasi perilaku negara, yang sebenarnya mengapa saya menyusun versi imperatif seperti itu: mengumpulkan input, melakukan hal-hal murni, mengeluarkan output. Semoga convert()bisa benar-benar murni dan menuai manfaat cachability, threadsafety, dll.

Saya juga menghargai bahwa jenis monadik dapat bermanfaat, terutama dalam jaringan pipa yang beroperasi pada jenis yang sebanding, tetapi tidak melihat mengapa IO harus menggunakan monad kecuali sudah ada dalam pipa seperti itu.

Apakah ada manfaat tambahan untuk berurusan dengan efek samping yang dibawa oleh pola IO monad, yang saya lewatkan?

Stu Cox
sumber
1
Anda harus menonton video ini . Keajaiban monad akhirnya terungkap tanpa menggunakan Kategori Teori atau Haskell. Ternyata monad diekspresikan secara sepele dalam JavaScript, dan merupakan salah satu enabler utama Ajax. Monad luar biasa. Itu adalah hal-hal sederhana, hampir diimplementasikan secara sepele, dengan kekuatan besar untuk mengelola kompleksitas. Tetapi memahaminya sangat sulit, dan kebanyakan orang, begitu mereka memiliki momen ah-ha, tampaknya kehilangan kemampuan untuk menjelaskannya kepada orang lain.
Robert Harvey
Video yang bagus, terima kasih. Saya benar-benar belajar tentang hal-hal ini dari intro JS ke pemrograman fungsional (kemudian membaca satu juta lebih ...). Meskipun setelah menonton itu, saya cukup yakin pertanyaan saya khusus untuk monad IO, yang tidak dibahas Crock dalam video itu.
Stu Cox
Hmm ... Bukankah AJAX dianggap sebagai bentuk I / O?
Robert Harvey
1
Perhatikan bahwa jenis maindalam program Haskell adalah IO ()- tindakan IO. Ini sebenarnya bukan fungsi sama sekali; itu sebuah nilai . Seluruh program Anda adalah nilai murni yang berisi instruksi yang memberi tahu runtime bahasa apa yang harus dilakukan. Semua hal yang tidak murni (benar-benar melakukan tindakan IO) berada di luar cakupan program Anda.
Wyzard --Hentikan Harming Monica--
Dalam contoh Anda, bagian monadik adalah saat Anda mengambil hasil dari satu perhitungan ( read_file) dan menggunakannya sebagai argumen ke yang berikutnya ( write_file). Jika Anda hanya memiliki urutan tindakan independen, Anda tidak perlu Monad.
lortabac

Jawaban:

14

Seluruh program pada akhirnya akan terkandung dalam monad IO, pada dasarnya.

Itulah bagian di mana saya pikir Anda tidak melihatnya dari sudut pandang Haskellers. Jadi kami memiliki program seperti ini:

module Main

main :: IO ()
main = do
  xmlData <- readFile "input.xml"
  let jsonData = convert xmlData
  writeFile "output.json" jsonData

convert :: String -> String
convert xml = ...

Saya pikir pendapat khas Haskeller tentang hal ini adalah convert, bagian yang murni:

  1. Mungkin bagian terbesar dari program ini, dan jauh lebih rumit daripada IObagian - bagiannya;
  2. Dapat beralasan dan diuji tanpa harus berurusan IOsama sekali.

Sehingga mereka tidak melihat ini sebagai convertmenjadi "terkandung" dalam IO, melainkan, karena yang terisolasi dari IO. Dari tipenya, apa pun yang convertdilakukannya tidak pernah bisa bergantung pada apa pun yang terjadi dalam suatu IOtindakan.

Ketika kode ini benar-benar dieksekusi, semuanya setelah membaca file tergantung pada status file itu sehingga masih akan menderita bug terkait negara yang sama dengan implementasi imperatif, jadi apakah Anda benar-benar mendapatkan sesuatu, sebagai programmer yang akan mempertahankan ini?

Saya akan mengatakan bahwa ini terbagi menjadi dua hal:

  1. Ketika program berjalan, nilai dari argumen untuk converttergantung pada keadaan file.
  2. Tapi apa yang convertfungsi tidak , yang tidak bergantung pada keadaan file. convertselalu memiliki fungsi yang sama , bahkan jika dipanggil dengan argumen yang berbeda pada titik yang berbeda.

Ini adalah poin yang agak abstrak, tetapi itu benar-benar kunci untuk apa yang dimaksud Haskellers ketika mereka membicarakan hal ini. Anda ingin menulis convertsedemikian rupa yang diberikan setiap argumen yang valid, maka akan menghasilkan hasil yang benar untuk argumen itu. Ketika Anda melihatnya seperti itu, fakta bahwa membaca file adalah operasi stateful tidak masuk ke dalam persamaan; yang penting adalah bahwa argumen apa pun yang diberikan padanya dan dari mana pun itu berasal, convertharus menanganinya dengan benar. Dan fakta bahwa kemurnian membatasi apa yang convertdapat dilakukan dengan inputnya menyederhanakan alasan itu.

Jadi jika convertmenghasilkan hasil yang salah dari beberapa argumen, dan readFilemengumpankannya dengan argumen seperti itu, kami tidak melihatnya sebagai bug yang diperkenalkan oleh negara . Ini bug dalam fungsi murni!

sakundim
sumber
Saya pikir ini adalah deskripsi terbaik (walaupun yang lain juga membantu menjelaskan hal-hal untuk saya), terima kasih.
Stu Cox
apakah perlu dicatat bahwa menggunakan monad dalam python mungkin memiliki manfaat lebih sedikit karena python hanya memiliki satu jenis (statis), dan karenanya tidak akan memberikan jaminan tentang apa pun?
jk.
7

Sulit untuk memastikan apa yang Anda maksud dengan "murni akademis", tetapi saya pikir jawabannya sebagian besar "tidak".

Seperti dijelaskan dalam Tackling the Awkward Squad oleh Simon Peyton Jones ( sangat disarankan membaca!), I / O monadik dimaksudkan untuk memecahkan masalah nyata dengan cara yang digunakan Haskell untuk menangani I / O. Baca contoh server dengan Permintaan dan Tanggapan, yang tidak akan saya salin di sini; ini sangat instruktif.

Haskell, tidak seperti Python, mendorong gaya komputasi "murni" yang dapat ditegakkan oleh sistem tipenya. Tentu saja, Anda bisa menggunakan disiplin diri ketika memprogram dengan Python untuk mematuhi gaya ini, tetapi bagaimana dengan modul yang tidak Anda tulis? Tanpa banyak bantuan dari sistem tipe (dan perpustakaan umum), monadic I / O mungkin kurang berguna dalam Python. Filsafat bahasa tidak hanya dimaksudkan untuk menegakkan pemisahan murni / tidak murni yang ketat.

Perhatikan bahwa ini mengatakan lebih banyak tentang filosofi berbeda Haskell dan Python daripada tentang bagaimana akademik monadik I / O. Saya tidak akan menggunakannya untuk Python.

Satu hal lagi. Kamu bilang:

Seluruh program pada akhirnya akan terkandung dalam monad IO, pada dasarnya.

Memang benar mainfungsi Haskell "hidup" IO, tetapi program Haskell yang sebenarnya dianjurkan untuk tidak menggunakan IOkapan pun itu tidak diperlukan. Hampir setiap fungsi yang Anda tulis yang tidak perlu dilakukan I / O tidak boleh memiliki tipe IO.

Jadi saya akan mengatakan dalam contoh terakhir Anda mendapatkannya mundur: maintidak murni (karena membaca dan menulis file) tetapi fungsi inti seperti convertmurni.

Andres F.
sumber
3

Mengapa IO tidak murni? Karena dapat mengembalikan nilai yang berbeda pada waktu yang berbeda. Ada ketergantungan pada waktu yang harus dipertanggungjawabkan, dengan satu atau lain cara. Ini bahkan lebih penting dengan evaluasi malas. Pertimbangkan program berikut:

main = do  
    putStrLn "Please enter your name"  
    name <- getLine
    putStrLn $ "Hello, " ++ name

Tanpa mono IO, mengapa prompt pertama akan mendapatkan output? Tidak ada yang bergantung padanya, jadi evaluasi malas berarti tidak akan pernah dituntut. Juga tidak ada yang mendorong prompt untuk menjadi output sebelum input dibaca. Sejauh menyangkut komputer, tanpa monad IO, dua ekspresi pertama tersebut sepenuhnya independen satu sama lain. Untungnya, namememberlakukan pesanan pada dua yang kedua.

Ada cara-cara lain untuk menyelesaikan masalah ketergantungan pada pesanan, tetapi menggunakan monad IO mungkin merupakan cara paling sederhana (dari sudut pandang bahasa setidaknya) untuk memungkinkan semuanya tetap di ranah fungsional yang malas, tanpa sedikit bagian kode imperatif. Ini juga yang paling fleksibel. Misalnya, Anda dapat dengan relatif mudah membangun pipa IO secara dinamis saat runtime berdasarkan input pengguna.

Karl Bielefeldt
sumber
2

Pemahaman saya (terbatas) tentang pemrograman fungsional adalah bahwa keadaan / efek samping harus diminimalkan dan dipisahkan dari logika stateless.

Itu bukan hanya pemrograman fungsional; itu biasanya ide bagus dalam bahasa apa pun. Jika Anda melakukan pengujian unit, cara Anda berpisah read_file(), convert()dan write_file()datang secara alami karena, meskipun convert()sejauh ini merupakan bagian yang paling kompleks dan terbesar dari kode, menulis tes untuk itu relatif mudah: yang Anda perlukan hanyalah parameter input . Menulis tes untuk read_file()dan write_file()sedikit lebih sulit (walaupun fungsinya sendiri hampir sepele) karena Anda perlu membuat dan / atau membaca hal-hal pada sistem file sebelum dan sesudah memanggil fungsi. Idealnya Anda akan membuat fungsi-fungsi seperti itu begitu sederhana sehingga Anda merasa nyaman tidak mengujinya dan dengan demikian menghemat banyak kesulitan.

Perbedaan antara Python dan Haskell di sini adalah bahwa Haskell memiliki pemeriksa tipe yang dapat membuktikan bahwa fungsi tidak memiliki efek samping. Dalam Python Anda perlu berharap bahwa tidak ada orang yang secara tidak sengaja memasukkan fungsi membaca file atau menulis ke convert()(katakanlah, read_config_file()). Di Haskell ketika Anda mendeklarasikan convert :: String -> Stringatau serupa, tanpa IOmonad, pemeriksa tipe akan menjamin bahwa ini adalah fungsi murni yang hanya bergantung pada parameter inputnya dan tidak ada yang lain. Jika seseorang mencoba memodifikasi convertuntuk membaca file konfigurasi, mereka akan dengan cepat melihat kesalahan kompiler yang menunjukkan bahwa mereka akan merusak kemurnian fungsi. (Dan mudah-mudahan mereka cukup masuk akal untuk read_config_filekeluar convertdan memberikan hasilnya convert, menjaga kemurnian.)

Curt J. Sampson
sumber