Dalam pemrograman fungsional, apakah variabel lokal yang bisa berubah tanpa efek samping masih dianggap sebagai "praktik buruk"?

23

Apakah memiliki variabel lokal yang dapat berubah dalam fungsi yang hanya digunakan secara internal, (misalnya fungsi tidak memiliki efek samping, setidaknya tidak sengaja) masih dianggap "tidak berfungsi"?

misalnya dalam pemeriksaan gaya program "Pemrograman fungsional dengan Scala" menganggap varpenggunaan apa pun sebagai buruk

Pertanyaan saya, jika fungsi tidak memiliki efek samping, apakah menulis kode gaya imperatif masih berkecil hati?

misal daripada menggunakan rekursi ekor dengan pola akumulator, apa yang salah dengan melakukan local untuk loop dan membuat mutable lokalListBuffer dan menambahkannya, selama input tidak berubah?

Jika jawabannya "ya, mereka selalu berkecil hati, bahkan jika tidak ada efek samping" lalu apa alasannya?

Eran Medan
sumber
3
Semua saran, nasihat, dll. Tentang topik yang pernah saya dengar merujuk pada keadaan yang bisa berubah bersama sebagai sumber kompleksitas. Apakah kursus itu dimaksudkan untuk dikonsumsi oleh pemula saja? Maka itu mungkin penyederhanaan sengaja yang dimaksudkan dengan baik.
Kilian Foth
3
@KilianFoth: Keadaan yang dapat berubah yang dibagikan merupakan masalah dalam konteks multithreaded, tetapi keadaan yang tidak dapat dibagikan dapat menyebabkan program menjadi sulit untuk dipikirkan juga.
Michael Shaw
1
Saya pikir menggunakan variabel bisa berubah lokal tidak selalu praktik yang buruk, tetapi itu bukan "gaya fungsional": Saya pikir tujuan dari kursus Scala (yang saya ambil selama musim gugur lalu) adalah untuk mengajarkan Anda pemrograman dalam gaya fungsional. Setelah Anda dapat dengan jelas membedakan antara gaya fungsional dan imperatif Anda dapat memutuskan kapan harus menggunakan yang mana (jika bahasa pemrograman Anda memungkinkan keduanya). varselalu tidak berfungsi. Scala memiliki optimasi malas dan rekursi ekor, yang memungkinkan untuk menghindari vars sepenuhnya.
Giorgio

Jawaban:

17

Satu hal yang benar-benar praktik buruk di sini adalah mengklaim bahwa sesuatu adalah fungsi murni padahal sebenarnya tidak.

Jika variabel yang dapat diubah digunakan dengan cara yang benar-benar dan sepenuhnya mandiri, fungsinya secara eksternal murni dan semua orang senang. Haskell sebenarnya mendukung ini secara eksplisit , dengan sistem tipe yang bahkan memastikan bahwa referensi yang dapat diubah tidak dapat digunakan di luar fungsi yang membuatnya.

Yang mengatakan, saya pikir berbicara tentang "efek samping" bukan cara terbaik untuk melihatnya (dan itulah sebabnya saya mengatakan "murni" di atas). Apa pun yang menciptakan ketergantungan antara fungsi dan kondisi eksternal membuat hal-hal lebih sulit untuk dipikirkan, dan itu termasuk hal-hal seperti mengetahui waktu saat ini atau menggunakan keadaan yang dapat diubah yang tersembunyi dengan cara yang tidak aman.

CA McCann
sumber
16

Masalahnya bukan kemampuan berubah-ubah, itu adalah kurangnya transparansi referensial.

Suatu hal yang transparan referensial dan referensi padanya harus selalu sama, sehingga fungsi transparan referensial akan selalu mengembalikan hasil yang sama untuk set input yang diberikan dan "variabel" yang direferensikan transparan benar-benar nilai daripada variabel, karena itu tidak bisa berubah. Anda dapat membuat fungsi transparan referensial yang memiliki variabel yang bisa berubah di dalamnya; itu bukan masalah. Namun, mungkin lebih sulit untuk memastikan fungsi tersebut transparan secara referensi, tergantung pada apa yang Anda lakukan.

Ada satu contoh yang bisa saya pikirkan di mana kemampuan harus digunakan untuk melakukan sesuatu yang sangat fungsional: memoisasi. Memoisasi adalah caching nilai dari suatu fungsi, sehingga tidak harus dihitung ulang; itu transparan referensial, tetapi menggunakan mutasi.

Tetapi dalam transparansi referensial umum dan immutabilitas berjalan bersama, selain variabel lokal yang bisa berubah dalam fungsi transparan dan memoisasi referensial, saya tidak yakin ada contoh lain di mana hal ini tidak terjadi.

Michael Shaw
sumber
4
Poin Anda tentang memoisasi sangat bagus. Perhatikan bahwa Haskell sangat menekankan transparansi referensial untuk pemrograman, tetapi perilaku evaluasi yang malas seperti memoisasi melibatkan sejumlah besar mutasi yang dilakukan oleh runtime bahasa di belakang layar.
CA McCann
@CA McCann: Saya pikir apa yang Anda katakan sangat penting: dalam bahasa fungsional runtime dapat menggunakan mutasi untuk mengoptimalkan perhitungan tetapi tidak ada konstruk dalam bahasa yang memungkinkan programmer untuk menggunakan mutasi. Contoh lain adalah loop sementara dengan variabel loop: di Haskell Anda dapat menulis fungsi rekursif ekor yang dapat diimplementasikan dengan variabel yang bisa berubah-ubah (untuk menghindari penggunaan stack), tetapi apa yang dilihat programmer adalah argumen fungsi yang tidak dapat diubah yang dilewatkan dari satu panggilan ke yang berikutnya.
Giorgio
@Michael Shaw: +1 untuk "Masalahnya bukan kemampuan berubah-ubah per se, itu adalah kurangnya transparansi referensial." Mungkin Anda dapat mengutip bahasa Bersih di mana Anda memiliki jenis keunikan: ini memungkinkan mutabilitas tetapi masih menjamin transparansi referensial.
Giorgio
@Iorgio: Saya tidak benar-benar tahu apa-apa tentang Bersih, walaupun saya pernah mendengarnya dari waktu ke waktu. Mungkin saya harus memeriksanya.
Michael Shaw
@Michael Shaw: Saya tidak tahu banyak tentang Clean, tapi saya tahu itu menggunakan tipe keunikan untuk memastikan transparansi referensial. Pada dasarnya, Anda dapat memodifikasi objek data dengan ketentuan bahwa setelah modifikasi Anda tidak memiliki referensi ke nilai lama. IMO ini mengilustrasikan poin Anda: transparansi referensial adalah poin yang paling penting, dan imutabilitas hanyalah satu cara yang mungkin untuk memastikannya.
Giorgio
8

Tidak baik untuk merebusnya menjadi "praktik yang baik" vs "praktik yang buruk". Scala mendukung nilai-nilai yang bisa berubah karena mereka memecahkan masalah-masalah tertentu jauh lebih baik daripada nilai-nilai yang tidak berubah, yaitu mereka yang sifatnya berulang.

Untuk perspektif, saya cukup yakin bahwa melalui CanBuildFromhampir semua struktur abadi yang disediakan oleh scala lakukan semacam mutasi secara internal. Intinya adalah bahwa apa yang mereka ungkapkan tidak berubah. Menjaga sebanyak mungkin nilai yang tidak berubah mungkin membantu membuat program lebih mudah untuk dipikirkan dan lebih sedikit rawan kesalahan .

Ini tidak berarti bahwa Anda perlu menghindari struktur dan nilai yang bisa berubah secara internal ketika Anda memiliki masalah yang lebih cocok dengan kemampuan berubah-ubah.

Dengan mengingat hal itu, banyak masalah yang biasanya memerlukan variabel yang dapat berubah (seperti perulangan) dapat diselesaikan dengan lebih baik dengan banyak fungsi tingkat tinggi yang disediakan oleh bahasa seperti Scala (peta / filter / lipat). Sadarilah itu.

KChaloux
sumber
2
Yap, saya hampir tidak pernah memerlukan loop untuk saat menggunakan koleksi Scala. map, filter, foldLeftDan forEach melakukan trik sebagian besar waktu, tetapi ketika mereka tidak, bisa merasa saya "OK" untuk kembali kepada kekerasan kode penting bagus. (selama tidak ada efek samping tentunya)
Eran Medan
3

Selain kemungkinan masalah dengan keselamatan ulir, Anda juga biasanya kehilangan banyak jenis pengaman. Loop imperatif memiliki tipe pengembalian Unitdan dapat mengambil cukup banyak ekspresi untuk input. Fungsi tingkat tinggi dan bahkan rekursi memiliki semantik dan tipe yang jauh lebih presisi.

Anda juga memiliki lebih banyak opsi untuk pemrosesan kontainer fungsional dibandingkan dengan loop imperatif. Dengan penting, pada dasarnya anda memiliki for, while, dan variasi kecil pada kedua seperti do...whiledan foreach.

Secara fungsional, Anda memiliki agregat, menghitung, memfilter, menemukan, flatMap, lipat, groupBy, lastIndexWhere, map, maxBy, minBy, partisi, scan, sortBy, sortWith, span, dan takeWhile, hanya untuk menyebutkan beberapa yang lebih umum dari Scala's perpustakaan standar. Ketika Anda terbiasa memiliki yang tersedia, forloop imperatif tampak terlalu mendasar dibandingkan.

Satu-satunya alasan nyata untuk menggunakan mutabilitas lokal adalah sangat sesekali untuk kinerja.

Karl Bielefeldt
sumber
2

Saya akan mengatakan itu sebagian besar ok. Terlebih lagi, membuat struktur dengan cara ini mungkin merupakan cara yang baik untuk meningkatkan kinerja dalam beberapa kasus. Clojure telah mengatasi masalah ini dengan menyediakan struktur data Transient .

Ide dasarnya adalah untuk memungkinkan mutasi lokal dalam lingkup terbatas, dan kemudian membekukan struktur sebelum mengembalikannya. Dengan cara ini, pengguna Anda masih dapat alasan tentang kode Anda seolah-olah itu murni, tetapi Anda dapat melakukan transformasi tempat ketika Anda perlu.

Seperti yang dikatakan tautan:

Jika pohon tumbang di hutan, apakah pohon itu mengeluarkan suara? Jika fungsi murni bermutasi beberapa data lokal untuk menghasilkan nilai kembali yang tidak dapat diubah, apakah itu boleh?

Simon Bergot
sumber
2

Tidak memiliki variabel yang dapat berubah lokal memang memiliki satu keunggulan - itu membuat fungsi lebih ramah terhadap utas.

Saya terbakar oleh variabel lokal seperti itu (tidak dalam kode saya, juga tidak memiliki sumber) yang menyebabkan korupsi data probabilitas rendah. Keamanan utas tidak disebutkan satu atau lain cara, tidak ada keadaan yang bertahan di seluruh panggilan dan tidak ada efek samping. Tidak terpikir oleh saya bahwa itu mungkin bukan thread yang aman, mengejar 1 dalam 100.000 data acak korupsi adalah rasa sakit kerajaan.

Loren Pechtel
sumber