Saya sering menemui pernyataan / argumen berikut:
- Bahasa pemrograman fungsional murni tidak memungkinkan efek samping (dan karena itu tidak banyak digunakan dalam praktik karena setiap program yang bermanfaat memang memiliki efek samping, misalnya ketika berinteraksi dengan dunia luar).
- Bahasa pemrograman fungsional murni tidak memungkinkan untuk menulis sebuah program yang mempertahankan keadaan (yang membuat pemrograman sangat canggung karena dalam banyak aplikasi Anda membutuhkan keadaan).
Saya bukan ahli dalam bahasa fungsional tetapi inilah yang saya mengerti tentang topik-topik ini sampai sekarang.
Mengenai poin 1, Anda dapat berinteraksi dengan lingkungan dalam bahasa murni fungsional tetapi Anda harus secara eksplisit menandai kode (fungsi) yang memperkenalkan efek samping (misalnya di Haskell dengan cara tipe monadik). Juga, sejauh yang saya tahu komputasi dengan efek samping (memperbarui data secara destruktif) juga harus dimungkinkan (menggunakan tipe monadik?) Walaupun itu bukan cara kerja yang disukai.
Mengenai poin 2, sejauh yang saya tahu Anda dapat mewakili negara dengan memasukkan nilai melalui beberapa langkah perhitungan (di Haskell, sekali lagi, menggunakan tipe monadik) tetapi saya tidak memiliki pengalaman praktis melakukan ini dan pemahaman saya agak kabur.
Jadi, apakah kedua pernyataan di atas benar dalam arti apa pun atau mereka hanya kesalahpahaman tentang bahasa murni fungsional? Jika mereka salah paham, bagaimana hal itu terjadi? Bisakah Anda menulis potongan kode (mungkin kecil) yang menggambarkan cara Haskell idiomatis untuk (1) menerapkan efek samping dan (2) menerapkan perhitungan dengan status?
sumber
Jawaban:
Untuk keperluan jawaban ini saya mendefinisikan "bahasa murni fungsional" yang berarti bahasa fungsional di mana fungsi-fungsi transparan secara referensial, yaitu memanggil fungsi yang sama beberapa kali dengan argumen yang sama akan selalu menghasilkan hasil yang sama. Saya percaya ini adalah definisi umum dari bahasa yang murni fungsional.
Cara termudah untuk mencapai transparansi referensial memang dengan melarang efek samping dan memang ada bahasa di mana itu terjadi (kebanyakan yang spesifik domain). Namun itu tentu saja bukan satu-satunya cara dan tujuan paling umum bahasa murni fungsional (Haskell, Clean, ...) memungkinkan efek samping.
Juga mengatakan bahwa bahasa pemrograman tanpa efek samping sedikit digunakan dalam praktiknya tidak benar-benar adil saya pikir - tentu saja tidak untuk bahasa domain spesifik, tetapi bahkan untuk bahasa tujuan umum, saya membayangkan sebuah bahasa bisa sangat berguna tanpa memberikan efek samping . Mungkin bukan untuk aplikasi konsol, tapi saya pikir aplikasi GUI dapat diimplementasikan dengan baik tanpa efek samping, katakanlah, paradigma reaktif fungsional.
Itu sedikit terlalu menyederhanakannya. Hanya memiliki sistem di mana fungsi efek samping perlu ditandai seperti itu (mirip dengan koreksi-benar di C ++, tetapi dengan efek samping umum) tidak cukup untuk memastikan transparansi referensial. Anda perlu memastikan bahwa suatu program tidak pernah dapat memanggil fungsi beberapa kali dengan argumen yang sama dan mendapatkan hasil yang berbeda. Anda bisa melakukannya dengan membuat hal-hal seperti
readLine
menjadi sesuatu yang bukan fungsi (itulah yang Haskell lakukan dengan IO monad) atau Anda bisa membuatnya mustahil untuk memanggil fungsi efek samping beberapa kali dengan argumen yang sama (itulah yang dilakukan Clean). Dalam kasus terakhir kompiler akan memastikan bahwa setiap kali Anda memanggil fungsi efek samping, Anda melakukannya dengan argumen baru, dan itu akan menolak program apa pun di mana Anda meneruskan argumen yang sama ke fungsi efek samping dua kali.Sekali lagi, bahasa yang murni fungsional mungkin sangat tidak memungkinkan keadaan yang bisa berubah, tetapi tentu saja mungkin untuk menjadi murni dan masih memiliki keadaan yang bisa berubah, jika Anda menerapkannya dengan cara yang sama seperti yang saya jelaskan dengan efek samping di atas. Keadaan yang benar-benar bisa berubah hanyalah bentuk lain dari efek samping.
Yang mengatakan, bahasa pemrograman fungsional pasti mencegah negara bisa berubah - yang murni terutama begitu. Dan saya tidak berpikir itu membuat pemrograman canggung - justru sebaliknya. Kadang-kadang (tetapi tidak semua yang sering) keadaan bisa berubah tidak dapat dihindari tanpa kehilangan kinerja atau kejelasan (itulah sebabnya bahasa seperti Haskell memang memiliki fasilitas untuk keadaan bisa berubah), tetapi paling sering itu bisa.
Saya pikir banyak orang hanya membaca "suatu fungsi harus menghasilkan hasil yang sama ketika dipanggil dengan argumen yang sama" dan menyimpulkan bahwa itu tidak mungkin untuk mengimplementasikan sesuatu seperti
readLine
atau kode yang mempertahankan keadaan bisa berubah. Jadi mereka sama sekali tidak menyadari "cheat" yang dapat digunakan bahasa murni untuk memperkenalkan hal-hal ini tanpa melanggar transparansi referensial.Keadaan yang bisa berubah juga sangat tidak mendukung dalam bahasa fungsional, sehingga tidak terlalu banyak lompatan untuk menganggap itu tidak diperbolehkan sama sekali dalam bahasa yang murni fungsional.
Berikut aplikasi di Pseudo-Haskell yang meminta nama pengguna dan menyapanya. Pseudo-Haskell adalah bahasa yang baru saja saya temukan, yang memiliki sistem IO Haskell, tetapi menggunakan sintaksis yang lebih konvensional, nama fungsi yang lebih deskriptif dan tidak memiliki
do
notasi (karena itu hanya akan mengalihkan perhatian dari bagaimana tepatnya IO monad bekerja):Petunjuk di sini adalah bahwa itu
readLine
adalah nilai tipeIO<String>
dancomposeMonad
merupakan fungsi yang mengambil argumen tipeIO<T>
(untuk beberapa tipeT
) dan argumen lain yang merupakan fungsi yang mengambil argumen tipeT
dan mengembalikan nilai tipeIO<U>
(untuk beberapa tipeU
).print
adalah fungsi yang mengambil string dan mengembalikan nilai tipeIO<void>
.Nilai tipe
IO<A>
adalah nilai yang "menyandikan" tindakan yang diberikan yang menghasilkan nilai tipeA
.composeMonad(m, f)
menghasilkanIO
nilai baru yang mengkodekan tindakanm
diikuti oleh tindakanf(x)
, di manax
nilai menghasilkan dengan melakukan tindakanm
.Status yang bisa berubah akan terlihat seperti ini:
Berikut
mutableVariable
adalah fungsi yang mengambil nilai dari jenis apa punT
dan menghasilkan aMutableVariable<T>
. FungsigetValue
mengambilMutableVariable
dan mengembalikan suatuIO<T>
yang menghasilkan nilai saat ini.setValue
mengambil aMutableVariable<T>
dan aT
dan mengembalikan sebuahIO<void>
yang menetapkan nilai.composeVoidMonad
sama seperticomposeMonad
kecuali bahwa argumen pertama adalah argumenIO
yang tidak menghasilkan nilai yang masuk akal dan argumen kedua adalah monad lain, bukan fungsi yang mengembalikan monad.Dalam Haskell ada beberapa gula sintaksis, yang membuat seluruh cobaan ini tidak terlalu menyakitkan, tetapi masih jelas bahwa keadaan yang bisa berubah adalah sesuatu yang tidak benar-benar ingin Anda lakukan bahasa.
sumber
counter
, yaituincreaseCounter(counter)
?main
akan menjadi salah satu yang benar-benar dieksekusi. Selain mengembalikan IO darimain
tidak ada cara untuk melakukanIO
tindakan (tanpa menggunakan fungsi jahat mengerikan yang adaunsafe
dalam nama mereka).IO
nilai-nilai yang merusak . Saya tidak mengerti apakah ia merujuk pada pencocokan pola, yaitu fakta bahwa Anda dapat mendekonstruksi nilai tipe data aljabar, tetapi seseorang tidak dapat menggunakan pencocokan pola untuk melakukan ini denganIO
nilai.IMHO Anda bingung karena ada perbedaan antara bahasa murni dan fungsi murni . Mari kita mulai dengan fungsinya. Suatu fungsi murni jika akan (diberi input yang sama) selalu mengembalikan nilai yang sama dan tidak menyebabkan efek samping yang dapat diamati. Contoh umum adalah fungsi matematika seperti f (x) = x * x. Sekarang pertimbangkan implementasi dari fungsi ini. Ini akan menjadi murni di sebagian besar bahasa bahkan mereka yang umumnya tidak dianggap bahasa fungsional murni misalnya ML. Bahkan metode Java atau C ++ dengan perilaku ini dapat dianggap sebagai murni.
Jadi apa itu bahasa murni? Sebenarnya orang mungkin berharap bahwa bahasa murni tidak membiarkan Anda mengekspresikan fungsi yang tidak murni. Mari kita sebut ini definisi idealis dari bahasa murni. Perilaku seperti itu sangat diinginkan. Mengapa? Nah hal yang menyenangkan tentang program yang hanya terdiri dari fungsi murni adalah Anda dapat mengganti aplikasi fungsi dengan nilainya tanpa mengubah arti program. Hal ini membuat alasan yang sangat mudah tentang program karena setelah Anda tahu hasilnya Anda bisa melupakan cara itu dihitung. Purity juga memungkinkan kompiler melakukan optimasi agresif tertentu.
Jadi bagaimana jika Anda memerlukan kondisi internal? Anda dapat meniru status dalam bahasa murni hanya dengan menambahkan status sebelum perhitungan sebagai parameter input dan status setelah perhitungan sebagai bagian dari hasil. Alih-alih
Int -> Bool
Anda mendapatkan sesuatu sepertiInt -> State -> (Bool, State)
. Anda cukup membuat ketergantungan secara eksplisit (yang dianggap praktik yang baik dalam paradigma pemrograman apa pun). BTW ada monad yang merupakan cara yang sangat elegan untuk menggabungkan fungsi meniru negara menjadi fungsi meniru negara yang lebih besar. Dengan cara ini Anda pasti dapat "mempertahankan status" dalam bahasa murni. Tetapi Anda harus membuatnya eksplisit.Jadi apakah ini berarti saya bisa berinteraksi dengan luar? Toh program yang bermanfaat harus berinteraksi dengan dunia nyata agar bermanfaat. Tetapi input dan output jelas tidak murni. Menulis byte tertentu ke file tertentu mungkin baik-baik saja untuk pertama kalinya. Tetapi menjalankan operasi yang sama persis untuk kedua kalinya mungkin mengembalikan kesalahan karena disk penuh. Jelas tidak ada bahasa murni (dalam arti idealistik) yang dapat menulis ke file.
Jadi kita dihadapkan pada dilema. Kami ingin sebagian besar fungsi murni tetapi beberapa efek samping benar-benar diperlukan dan yang tidak murni. Sekarang definisi realistis dari bahasa murni adalah bahwa harus ada beberapa cara untuk memisahkan bagian murni dari bagian lain. Mekanisme harus memastikan bahwa tidak ada operasi yang tidak murni menyelinap masuk ke bagian murni.
Di Haskell ini dilakukan dengan tipe IO. Anda tidak dapat merusak hasil IO (tanpa mekanisme yang tidak aman). Dengan demikian Anda hanya dapat memproses hasil IO dengan fungsi yang didefinisikan dalam modul IO sendiri. Untungnya ada kombinator yang sangat fleksibel yang memungkinkan Anda untuk mengambil hasil IO dan memprosesnya dalam suatu fungsi selama fungsi itu mengembalikan hasil IO yang lain. Combinator ini disebut bind (atau
>>=
) dan memiliki tipeIO a -> (a -> IO b) -> IO b
. Jika Anda menggeneralisasi konsep ini, Anda tiba di kelas monad dan IO merupakan turunannya.sumber
unsafe
namanya) tidak memenuhi definisi idealistik Anda. Tidak ada fungsi tidak murni di Haskell (lagi-lagi mengabaikanunsafePerformIO
dan bekerja sama).readFile
danwriteFile
akan selalu mengembalikan nilai yang samaIO
, mengingat argumen yang sama. Jadi misal dua cuplikan kodelet x = writeFile "foo.txt" "bar" in x >> x
danwriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
akan melakukan hal yang sama.IO Something
? Jika demikian, sangat mungkin untuk memanggil fungsi IO dua kali dengan argumen yang sama:putStrLn "hello" >> putStrLn "hello"
- di sini kedua panggilanputStrLn
memiliki argumen yang sama. Tentu saja itu bukan masalah karena, seperti yang saya katakan sebelumnya, kedua panggilan akan menghasilkan nilai IO yang sama.writeFile "foo.txt" "bar"
tidak dapat menyebabkan kesalahan karena mengevaluasi panggilan fungsi tidak menjalankan tindakan. Jika Anda mengatakan bahwa dalam contoh saya sebelumnya versi denganlet
hanya memiliki satu kesempatan untuk menyebabkan kegagalan IO sementara versi tanpalet
memiliki dua, Anda salah. Kedua versi memiliki dua peluang untuk kegagalan IO. Karenalet
versi mengevaluasi panggilanwriteFile
hanya sekali sementara versi tanpalet
mengevaluasinya dua kali, Anda dapat melihat bahwa itu tidak masalah seberapa sering fungsi dipanggil. Yang penting hanyalah seberapa sering hasilnya ...putStrLn
Fungsi mengambil tepat satu argumen, yang merupakan tipeString
. Jika Anda tidak percaya padaku, melihat jenisnya:String -> IO ()
. Itu tentu saja tidak mengambil argumen tipe apa punIO
- itu menghasilkan nilai tipe itu.