Mengapa lebih mudah untuk berpikir tentang bahasa pemrograman dan program yang tidak memiliki efek samping?

8

Saya membaca "The Why of Y" dari Richard P. Gabriel . Ini adalah artikel yang mudah dibaca tentang Y combinator, yang sangat jarang. Artikel dimulai dengan definisi rekursif dari fungsi faktorial:

(letrec ((f (lambda (n)
              (if (< n 2) 1 (* n (f (- n 1)))))))
  (f 10))

Dan menjelaskan bahwa letrecdapat didefinisikan dengan efek samping:

(let ((f #f))
  (set! f (lambda (n)
            (if (< n 2) 1 (* n (f (- n 1))))))
  (f 10))

Dan sisa artikel menjelaskan, bahwa dimungkinkan juga untuk mendefinisikan letrecdengan kombinator Y:

(define (Y f)
  (let ((g (lambda (h)
             (lambda (x)
               ((f (h h)) x)))))
    (g g)))

(let ((f (Y (lambda (fact)
              (lambda (n)
                (if (< n 2) 1 (* n (fact (- n 1)))))))))
  (f 10))

Jelas ini jauh lebih rumit daripada versi dengan efek sampingnya. Alasan mengapa lebih disukai untuk memilih Y combinator daripada efek samping diberikan hanya dengan pernyataan:

Lebih mudah untuk berpikir tentang bahasa pemrograman dan program yang tidak memiliki efek samping.

Ini tidak dijelaskan lebih lanjut. Saya mencoba mencari penjelasan.

ceving
sumber
Kalimat "yang lebih mudah untuk dipikirkan" itu adalah propaganda murni. Itu selalu diberikan sebagai artikel iman - tidak ada bukti yang diperlukan atau ditawarkan - dan ketika dianalisis secara kritis bahkan tidak lulus tes tertawa. Seperti yang Anda catat, sangat sepele bahwa versi Y Combinator lebih dari dua kali lebih rumit, dan karenanya lebih sulit untuk dipahami dan dipikirkan!
Mason Wheeler
5
@MasonWheeler Melewati objek yang dapat diubah ke beberapa metode membuatnya sulit untuk mengetahui di mana ia digunakan murni sebagai input dan di mana ia dimutasi di tempat. Alternatif fungsional - mengembalikan salinan baru objek - membuatnya jelas. Saya tidak akan mengatakan yang murni selalu lebih baik, tetapi sulit untuk mengklaim bahwa grafik besar benda yang bisa berubah mudah untuk dipikirkan. Terlalu banyak konteks tak terlihat yang terlibat.
Doval
@Doval Bagaimana "diperjelas" ketika Anda sekarang memiliki banyak salinan objek Anda berlarian, beberapa di antaranya sudah usang, yang lain kanonis, dan sekarang Anda harus tetap meluruskannya? Itu terdengar lebih membingungkan! (Atau, alternatif, Anda harus memastikan bahwa ada yang tidak ada referensi untuk setiap salinan sekunder, yang merupakan tugas persis sama dengan manajemen memori manual, yang FP ditemukan begituuuuuuuu membingungkan dan sulit untuk alasan tentang itu diciptakan pengumpulan sampah untuk menghindari kebutuhan untuk melakukannya!)
Mason Wheeler
2
@MasonWheeler Bahkan ketika data seharusnya berubah, Anda ingin mengendalikan siapa yang mengubahnya. Anda ingin meneruskannya ke metode yang tidak seharusnya mengubahnya, tetapi seseorang dapat mengacau dan memperkenalkan bug yang akhirnya memutasi data. Kemudian Anda akhirnya membuat "salinan defensif" (yang sebenarnya merupakan rekomendasi dalam buku Java Efektif!) Dan melakukan lebih banyak pekerjaan / menghasilkan lebih banyak sampah daripada menggunakan struktur data yang tidak dapat diubah dari awal. Fakta bahwa data akan berubah tidak pernah menghalangi siapa pun menggunakan string atau tipe numerik yang tidak berubah.
Doval
2
@MasonWheeler FP bahasa tidak menghasilkan banyak sampah, kalau tidak mereka tidak akan berguna. Itu bukan cara mereka bekerja "di belakang layar". "Lebih mudah untuk beralasan" biasanya merujuk pada penalaran yang sama, yang bukan masalah tertawa. Penalaran kesetaraan dapat dilakukan dalam banyak paradigma, dengan keberhasilan yang berbeda-beda, tetapi dalam bahasa FP biasanya lebih mudah, dan itu merupakan kemenangan besar (meskipun dengan mengorbankan hal-hal lain; semuanya merupakan pertukaran dalam hidup).
Andres F.

Jawaban:

13

Jelas, Anda dapat menemukan contoh fungsi murni yang sangat sulit dibaca yang melakukan perhitungan yang sama dengan fungsi dengan efek samping yang jauh lebih mudah dibaca. Terutama ketika Anda menggunakan transformasi mekanis seperti Y-combinator untuk sampai pada solusi. Bukan itu yang dimaksud dengan "lebih mudah untuk berpikir tentang."

Alasan mengapa lebih mudah untuk beralasan tentang fungsi tanpa efek samping adalah Anda hanya perlu khawatir dengan input dan output. Dengan efek samping, Anda juga harus khawatir tentang berapa kali fungsi dipanggil, urutan apa fungsi itu dipanggil, data apa yang dibuat dalam fungsi, data apa yang dibagikan, dan data apa yang disalin. Dan semua informasi itu untuk fungsi apa pun yang dapat dipanggil internal ke fungsi yang Anda panggil, dan secara internal rekursif untuk fungsi-fungsi itu, dan sebagainya.

Efek ini jauh lebih mudah dilihat dalam kode produksi dengan beberapa lapisan daripada pada fungsi contoh mainan. Sebagian besar itu berarti Anda dapat lebih mengandalkan hanya pada tanda tangan jenis fungsi. Anda benar-benar memperhatikan beban efek samping jika Anda melakukan pemrograman fungsional murni untuk sementara waktu kemudian kembali ke sana.

Karl Bielefeldt
sumber
10

Properti bahasa yang menarik tanpa efek samping adalah memperkenalkan paralelisme, konkurensi, atau asinkron tidak dapat mengubah arti program. Itu bisa membuatnya lebih cepat. Atau itu bisa membuatnya lebih lambat. Tapi itu tidak bisa salah.

Ini membuatnya sepele untuk secara otomatis memparalelkan program. Jadi sepele, pada kenyataannya, bahwa Anda biasanya berakhir dengan terlalu banyak paralelisme! Tim GHC bereksperimen dengan paralelisasi otomatis. Mereka menemukan bahwa bahkan program sederhana dapat diuraikan menjadi ratusan, bahkan ribuan utas. Overhead dari semua utas tersebut akan membanjiri potensi percepatan oleh beberapa urutan besarnya.

Jadi, untuk paralelisasi otomatis dari program fungsional, masalahnya menjadi "bagaimana Anda mengelompokkan operasi atom kecil menjadi ukuran yang berguna dari potongan paralel", sebagai lawan dari program yang tidak murni, di mana masalahnya adalah "bagaimana Anda memecah operasi monolitik besar menjadi berguna ukuran potongan paralel ". Hal yang menyenangkan tentang ini adalah bahwa yang pertama dapat dilakukan secara heuristik (ingat: jika Anda salah, hal terburuk yang dapat terjadi adalah bahwa program berjalan sedikit lebih lambat daripada yang seharusnya), sedangkan yang terakhir setara dengan menyelesaikan Menghentikan Masalah (dalam kasus umum), dan jika Anda salah, program Anda hanya akan crash (jika Anda beruntung!) Atau mengembalikan hasil yang salah secara halus (dalam kasus terburuk).

Jörg W Mittag
sumber
bisa juga jalan buntu atau livelock, bukan berarti lebih baik ...
Deduplicator
6

Bahasa dengan efek samping menggunakan analisis aliasing untuk melihat apakah lokasi memori mungkin perlu dimuat ulang setelah pemanggilan fungsi. Seberapa konservatif analisis ini tergantung pada bahasanya.

Untuk C, ini harus sangat konservatif, karena bahasa tidak aman.

Untuk Java dan C # ini tidak harus sekonservatif karena peningkatan keamanan jenisnya.

Menjadi terlalu konservatif mencegah optimalisasi pemuatan.

Analisis seperti itu tidak perlu (atau sepele tergantung pada bagaimana Anda melihatnya) dalam bahasa tanpa efek samping.

Erik Eidt
sumber
Perhatikan bahwa aliasing hanya mungkin dengan kedua variabel bisa berubah dan referensi. Bahasa dengan hanya satu atau yang lain tidak memiliki masalah ini
gardenhead
4

Selalu ada optimisasi untuk mengambil keuntungan dari asumsi apa pun yang Anda berikan. Operasi penataan ulang muncul di pikiran.

Satu contoh lucu yang muncul di benak sebenarnya muncul dalam beberapa bahasa majelis lama. Secara khusus MIPS memiliki aturan bahwa instruksi setelah lompatan bersyarat dieksekusi, terlepas dari cabang mana yang diambil. Jika Anda tidak menginginkan ini, Anda meletakkan NOP setelah melompat. Ini dilakukan karena cara pipa MIPS terstruktur. Ada warung 1 siklus alami yang dibangun ke dalam eksekusi lompat bersyarat, jadi Anda mungkin juga melakukan sesuatu yang berguna dengan siklus itu!

Compiler akan sering mencari operasi yang perlu dilakukan pada kedua cabang dan geser ke dalam slot itu, untuk menghasilkan kinerja yang lebih sedikit. Namun, jika kompiler tidak dapat melakukan itu, tetapi dapat menunjukkan bahwa tidak ada efek samping pada operasi, kompiler secara oportunis dapat memasukkannya ke tempat itu. Jadi, pada satu jalur, kode akan menjalankan satu instruksi lebih cepat. Di jalur lain, tidak ada salahnya dilakukan.

Cort Ammon
sumber
1

"letrec dapat didefinisikan dengan efek samping ..." Saya tidak melihat efek samping dalam definisi Anda. Ya, ini menggunakan set!cara khas untuk menghasilkan efek samping dalam Skema, tetapi dalam kasus ini tidak ada efek samping - karena fmurni lokal, tidak dapat dirujuk oleh fungsi apa pun selain lokal. Oleh karena itu bukan efek samping yang terlihat dari ruang lingkup eksternal. Apa yang dilakukan kode ini adalah meretas batasan dalam cakupan Skema yang secara default tidak mengizinkan definisi lambda merujuk pada dirinya sendiri.

Beberapa bahasa lain memiliki deklarasi di mana ekspresi yang digunakan untuk menghasilkan nilai konstanta dapat merujuk ke konstanta itu sendiri. Dalam bahasa seperti itu, definisi ekuivalen yang tepat dapat digunakan, tetapi jelas ini tidak menghasilkan efek samping, karena hanya konstanta yang digunakan. Lihat, misalnya, program Haskell yang setara ini:

let f = \ n -> if n < 2 
                 then 1 
                 else n*(f (n-1)) 
        in (f 5)

(yang mengevaluasi hingga 120).

Ini jelas tidak memiliki efek samping (seperti agar fungsi di Haskell memiliki efek samping, ia harus mengembalikan hasilnya dengan membungkus Monad, tetapi tipe yang dikembalikan di sini adalah tipe numerik biasa), tetapi secara struktural identik dengan kode kode yang Anda kutip.

Periata Breatta
sumber
Secara umum itu adalah efek samping, karena letbisa mengembalikan fungsi lokal.
ceving
2
@ceving - walaupun begitu, ini bukan efek samping, karena modifikasi lokasi penyimpanan terbatas pada waktu yang dapat terjadi pada waktu sebelum kode lain dapat membacanya . Agar efek samping menjadi nyata, harus mungkin bagi beberapa agen eksternal untuk memperhatikan bahwa itu telah terjadi; dalam hal ini, tidak mungkin terjadi.
Periata Breatta
0

Ini tidak dijelaskan lebih lanjut. Saya mencoba mencari penjelasan.

Itu adalah sesuatu yang melekat pada banyak dari kita yang telah mendebug basis kode besar tetapi Anda harus berurusan dengan skala yang cukup besar di tingkat pengawas untuk waktu yang cukup lama untuk menghargainya. Ini seperti memahami pentingnya berada di posisi dalam Poker. Pada awalnya itu tidak tampak seperti keuntungan yang berguna untuk pergi terakhir di akhir setiap belokan sampai Anda mencatat sejarah tangan sejuta tangan dan menyadari bahwa Anda memenangkan jauh lebih banyak uang di posisi daripada keluar.

Yang mengatakan, saya tidak setuju dengan gagasan bahwa perubahan ke variabel lokal adalah efek samping. Dari pandangan saya, suatu fungsi tidak menyebabkan efek samping jika tidak memodifikasi apa pun di luar ruang lingkupnya, bahwa apa pun yang disentuhnya dan dirusak tidak akan memengaruhi apa pun di bawah tumpukan panggilan atau memori atau sumber daya apa pun yang tidak diperoleh fungsi itu sendiri. .

Secara umum, hal tersulit untuk dipikirkan dalam basis kode yang rumit dan berskala besar yang tidak memiliki prosedur pengujian paling sempurna yang bisa dibayangkan adalah manajemen negara yang gigih, seperti semua perubahan pada objek granular dalam dunia video game saat Anda mengarungi puluhan ribuan fungsi saat mencoba mempersempit daftar tersangka yang tak berujung yang mana sebenarnya menyebabkan invarian seluruh sistem dilanggar ("ini seharusnya tidak pernah terjadi, siapa yang melakukannya?"). Jika tidak ada yang berubah di luar fungsi, maka tidak mungkin menyebabkan kerusakan pusat.

Tentu saja ini tidak mungkin dilakukan dalam semua kasus. Setiap aplikasi yang memperbarui basis data yang disimpan pada mesin yang berbeda, pada dasarnya, dirancang untuk menyebabkan efek samping, serta aplikasi apa pun yang dirancang untuk memuat dan menulis file. Tetapi ada banyak lagi yang dapat kita lakukan tanpa efek samping dalam banyak fungsi dan banyak program jika, misalnya, kita memiliki pustaka yang kaya akan struktur data yang tidak dapat diubah dan merangkul pola pikir ini lebih jauh.

Lucunya John Carmack telah melompat pada LISP dan ikut-ikutan meskipun mulai pada hari-hari C coding paling mikro-tuned. Saya menemukan diri saya melakukan hal yang serupa, meskipun saya masih menggunakan banyak C. Begitulah sifat pragmatis, saya pikir, yang telah menghabiskan bertahun-tahun melakukan debugging dan mencoba mencari alasan tentang sistem berskala besar yang kompleks secara keseluruhan dari tingkat pengawas. Bahkan yang secara mengejutkan kuat dan tanpa bug dalam jumlah besar masih dapat membuat Anda merasa tidak nyaman bahwa ada sesuatu yang salah yang bersembunyi di sudut jika ada banyak kondisi persisten kompleks yang dimodifikasi di antara grafik panggilan fungsi yang saling terhubung paling kompleks di antara jutaan baris kode. Bahkan jika setiap antarmuka diuji dengan unit test dan semua lulus, ada

Dalam praktiknya saya sering menemukan pemrograman fungsional membuatnya lebih sulit untuk memahami suatu fungsi. Itu masih memutar otak saya menjadi tikungan dan simpul, terutama dengan logika rekursif yang kompleks. Tetapi semua overhead intelektual yang terkait dengan mencari tahu beberapa fungsi yang ditulis dalam bahasa fungsional dikerdilkan oleh sistem yang kompleks dengan status persisten yang diubah di puluhan ribu fungsi, di mana setiap fungsi yang menyebabkan efek samping menambah total kompleksitas penalaran tentang kebenaran seluruh sistem secara keseluruhan.

Perhatikan bahwa Anda tidak memerlukan bahasa fungsional murni untuk membuat fungsi menghindari efek samping. Status lokal yang diubah dalam fungsi tidak dihitung sebagai efek samping, seperti forvariabel penghitung loop lokal ke fungsi tidak dihitung sebagai efek samping. Saya bahkan menulis kode C saat ini dengan tujuan menghindari efek samping bila mungkin dan telah membuat sendiri perpustakaan struktur data yang tidak dapat diubah dan aman yang dapat dimodifikasi sebagian sementara sisa data disalin dangkal, dan itu telah membantu saya banyak alasan tentang kebenaran sistem saya. Saya sangat tidak setuju dengan penulis dalam hal itu. Setidaknya dalam C dan C ++ dalam perangkat lunak mission-critical, suatu fungsi dapat didokumentasikan sebagai tidak memiliki efek samping jika tidak menyentuh apa pun yang dapat mempengaruhi dunia di luar fungsi.


sumber