Saya diakui bias sebagai orang yang menerapkan konsep-konsep seperti itu di C ++ oleh bahasa dan sifatnya, serta domain saya, dan bahkan cara kami menggunakan bahasa. Tetapi mengingat hal-hal ini, saya pikir desain abadi adalah aspek yang paling tidak menarik ketika datang untuk menuai sebagian besar manfaat yang terkait dengan pemrograman fungsional, seperti keselamatan benang, kemudahan penalaran tentang sistem, menemukan lebih banyak penggunaan kembali untuk fungsi (dan menemukan kita bisa gabungkan mereka dalam urutan apa pun tanpa kejutan yang tidak menyenangkan), dll.
Ambil contoh C ++ sederhana ini (diakui tidak dioptimalkan karena kesederhanaan untuk menghindari mempermalukan diri sendiri di depan para ahli pemrosesan gambar di luar sana):
// Inputs an image and outputs a new one with the specified size.
Image resized_image(const Image& src, int new_w, int new_h)
{
Image dst(new_w, new_h);
for (int y=0; y < new_h; ++y)
{
for (int x=0; x < new_w; ++x)
dst[y][x] = src.sample(x / (float)new_w, y / (float)new_h);
}
return dst;
}
Sementara implementasi dari fungsi itu bermutasi status lokal (dan sementara) dalam bentuk dua variabel counter dan gambar lokal sementara untuk output, ia tidak memiliki efek samping eksternal. Ini input gambar dan output yang baru. Kita bisa multithread ke konten hati kita. Mudah untuk dipikirkan, mudah untuk diuji secara menyeluruh. Ini pengecualian-aman karena jika ada yang melempar, gambar baru secara otomatis dibuang dan kita tidak perlu khawatir tentang mengembalikan efek samping eksternal (tidak ada gambar eksternal yang dimodifikasi di luar ruang lingkup fungsi, jadi untuk berbicara).
Saya melihat sedikit yang bisa didapat, dan berpotensi banyak yang hilang, dengan menghasilkan Image
tidak berubah dalam konteks di atas, dalam C ++, kecuali untuk berpotensi membuat fungsi di atas lebih sulit untuk diimplementasikan, dan mungkin sedikit kurang efisien.
Kemurnian
Jadi fungsinya murni (bebas dari eksternal efek samping ) sangat menarik bagi saya, dan saya menekankan pentingnya memberi mereka sering kepada anggota tim bahkan di C ++. Tetapi desain yang tidak dapat diubah, diterapkan pada umumnya tidak ada konteks dan nuansa, hampir tidak menarik bagi saya karena, mengingat sifat imperatif bahasa, sering berguna dan praktis untuk dapat bermutasi beberapa objek temporer lokal dalam proses efisien (keduanya untuk pengembang dan perangkat keras) menerapkan fungsi murni.
Menyalin Struktur Berat yang Murah
Properti kedua yang paling berguna yang saya temukan adalah kemampuan untuk dengan murah menyalin struktur data yang sangat besar di sekitar ketika biaya melakukannya, seperti yang sering dikeluarkan untuk membuat fungsi murni mengingat sifat input / output yang ketat, akan non-sepele. Ini tidak akan menjadi struktur kecil yang dapat ditampung di tumpukan. Mereka akan menjadi besar, struktur yang kuat, seperti keseluruhan Scene
untuk video game.
Dalam hal itu penyalinan overhead dapat mencegah peluang untuk paralelisme yang efektif, karena mungkin sulit untuk memaralelkan fisika dan rendering secara efektif tanpa mengunci dan menghambat satu sama lain jika fisika bermutasi adegan yang renderer secara bersamaan mencoba menggambar, sementara secara bersamaan memiliki fisika dalam menyalin seluruh adegan permainan hanya untuk menghasilkan satu frame dengan fisika yang diterapkan mungkin sama-sama tidak efektif. Namun, jika sistem fisika 'murni' dalam arti bahwa itu hanya memasukkan adegan dan menghasilkan yang baru dengan fisika yang diterapkan, dan kemurnian seperti itu tidak datang pada biaya overhead menyalin astronomi, itu bisa dengan aman beroperasi secara paralel dengan penyaji tanpa menunggu yang lain.
Jadi kemampuan untuk dengan murah menyalin data yang sangat besar dari keadaan aplikasi Anda dan menghasilkan versi yang baru dan dimodifikasi dengan biaya minimal untuk pemrosesan dan penggunaan memori dapat benar-benar membuka pintu baru untuk kemurnian dan paralelisme yang efektif, dan di sana saya menemukan banyak pelajaran untuk dipelajari dari bagaimana struktur data persisten diimplementasikan. Tetapi apa pun yang kita buat menggunakan pelajaran seperti itu tidak harus sepenuhnya gigih, atau menawarkan antarmuka yang tidak dapat diubah (mungkin menggunakan copy-on-write, misalnya, atau "builder / transient"), untuk mencapai kemampuan ini menjadi murah. untuk menyalin dan memodifikasi hanya bagian dari salinan tanpa menggandakan penggunaan memori dan akses memori dalam pencarian kami untuk paralelisme dan kemurnian dalam fungsi / sistem / pipa kami.
Kekekalan
Akhirnya ada kekekalan yang saya anggap paling tidak menarik dari ketiganya, tapi itu bisa menegakkan, dengan tangan besi, ketika desain objek tertentu tidak dimaksudkan untuk digunakan sebagai temporaries lokal untuk fungsi murni, dan sebagai gantinya dalam konteks yang lebih luas, yang berharga jenis "kemurnian tingkat objek", seperti dalam semua metode tidak lagi menyebabkan efek samping eksternal (tidak lagi bermutasi variabel anggota di luar lingkup lokal langsung dari metode).
Dan sementara saya menganggapnya sebagai yang paling tidak menarik dari ketiga bahasa ini seperti C ++, ini tentu saja dapat menyederhanakan pengujian dan keamanan utas dan alasan objek yang tidak sepele. Ini dapat menjadi beban untuk bekerja dengan jaminan bahwa suatu objek tidak dapat diberikan kombinasi keadaan unik di luar konstruktornya, misalnya, dan bahwa kita dapat dengan bebas menyebarkannya, bahkan dengan referensi / penunjuk tanpa bersandar pada kesegaran dan membaca. hanya iterator dan handle dan semacamnya, sambil menjamin (well, setidaknya sebanyak yang kami bisa dalam bahasa) bahwa konten aslinya tidak akan dimutasi.
Tetapi saya menemukan ini properti yang paling tidak menarik karena sebagian besar objek yang saya lihat bermanfaat digunakan sementara, dalam bentuk yang bisa berubah, untuk mengimplementasikan fungsi murni (atau bahkan konsep yang lebih luas, seperti "sistem murni" yang mungkin menjadi objek atau rangkaian dari berfungsi dengan efek pamungkas hanya memasukkan sesuatu dan menghasilkan sesuatu yang baru tanpa menyentuh yang lain), dan saya pikir kekekalan yang dibawa ke ekstremitas dalam bahasa yang sangat penting adalah tujuan yang agak kontraproduktif. Saya akan menerapkannya dengan hemat untuk bagian-bagian basis kode di mana itu sangat membantu.
Akhirnya:
[...] akan terlihat bahwa struktur data persisten tidak cukup untuk menangani skenario di mana satu utas membuat perubahan yang terlihat oleh utas lainnya. Untuk ini, tampaknya kita harus menggunakan perangkat seperti atom, referensi, memori transaksional perangkat lunak, atau bahkan kunci klasik dan mekanisme sinkronisasi.
Tentu saja jika desain Anda meminta modifikasi (dalam arti desain pengguna-akhir) agar dapat dilihat oleh banyak utas secara bersamaan ketika mereka terjadi, kami kembali ke sinkronisasi atau setidaknya papan gambar untuk mencari tahu beberapa cara canggih untuk menangani hal ini ( Saya telah melihat beberapa contoh yang sangat rumit yang digunakan oleh para ahli yang berurusan dengan masalah-masalah seperti ini dalam pemrograman fungsional).
Tetapi saya telah menemukan, begitu Anda mendapatkan penyalinan dan kemampuan semacam itu untuk mengeluarkan versi yang dimodifikasi sebagian dari struktur besar yang murah, seperti yang akan Anda peroleh dengan struktur data yang persisten sebagai contoh, ia sering membuka banyak pintu dan peluang yang mungkin Anda miliki. tidak pernah memikirkan sebelumnya untuk memparalelkan kode yang dapat berjalan sepenuhnya secara independen satu sama lain dalam semacam pipa paralel I / O yang ketat. Bahkan jika beberapa bagian dari algoritma harus serial, Anda mungkin menunda pemrosesan itu menjadi satu utas tetapi menemukan bahwa bersandar pada konsep-konsep ini telah membuka pintu dengan mudah, dan tanpa khawatir, sejajar dengan 90% dari pekerjaan besar, misalnya