Saya mengalami kesulitan menemukan sumber daya tentang cara menulis program dengan gaya fungsional. Topik paling maju yang bisa saya temukan dibahas secara online adalah menggunakan pengetikan struktural untuk mengurangi hierarki kelas; kebanyakan hanya berurusan dengan cara menggunakan peta / lipat / kurangi / dll untuk mengganti loop imperatif.
Apa yang ingin saya temukan adalah diskusi mendalam tentang implementasi OOP dari program non-sepele, keterbatasannya, dan bagaimana cara memperbaikinya dalam gaya fungsional. Bukan hanya algoritma atau struktur data, tetapi sesuatu dengan beberapa peran dan aspek yang berbeda - video game mungkin. Ngomong-ngomong saya membaca Pemrograman Fungsional Dunia Nyata oleh Tomas Petricek, tetapi saya masih menginginkan lebih.
Jawaban:
Definisi Pemrograman Fungsional
Pengantar The Joy of Clojure mengatakan yang berikut:
Pemrograman dalam Scala 2nd Edition hlm. 10 memiliki definisi berikut:
Jika kami menerima definisi pertama, maka satu-satunya hal yang perlu Anda lakukan untuk membuat kode Anda "fungsional" adalah mengubah loop Anda ke dalam. Definisi kedua termasuk kekekalan.
Fungsi Kelas Pertama
Bayangkan Anda saat ini mendapatkan Daftar Penumpang dari objek Bus Anda dan Anda mengulanginya mengurangi rekening bank setiap penumpang dengan jumlah ongkos bus. Cara fungsional untuk melakukan tindakan yang sama akan memiliki metode pada Bus, mungkin disebut forEachPassenger yang mengambil fungsi dari satu argumen. Kemudian Bus akan beralih dari penumpangnya namun yang terbaik dilakukan dan kode klien Anda yang menetapkan ongkos untuk perjalanan akan dimasukkan ke dalam fungsi dan diteruskan ke untuk setiap Penumpang. Voila! Anda menggunakan pemrograman fungsional.
Imperatif:
Fungsional (menggunakan fungsi anonim atau "lambda" di Scala):
Versi Scala yang lebih manis:
Fungsi Non-kelas
Jika bahasa Anda tidak mendukung fungsi kelas satu, ini bisa menjadi sangat jelek. Di Java 7 atau sebelumnya, Anda harus menyediakan antarmuka "Objek Fungsional" seperti ini:
Kemudian kelas Bus menyediakan iterator internal:
Akhirnya, Anda meneruskan objek fungsi anonim ke Bus:
Java 8 memungkinkan variabel lokal untuk ditangkap lingkup fungsi anonim, tetapi dalam versi sebelumnya, varibales tersebut harus dinyatakan final. Untuk menyiasatinya, Anda mungkin perlu membuat kelas pembungkus MutableReference. Berikut adalah kelas khusus integer yang memungkinkan Anda menambahkan penghitung lingkaran ke kode di atas:
Bahkan dengan keburukan ini, kadang-kadang bermanfaat untuk menghilangkan logika yang rumit dan berulang dari loop yang tersebar di seluruh program Anda dengan menyediakan iterator internal.
Keburukan ini telah diperbaiki di Java 8, tetapi penanganan pengecualian yang diperiksa di dalam fungsi kelas satu masih benar-benar jelek dan Java masih membawa asumsi bisa berubah-ubah dalam semua koleksinya. Yang membawa kita ke tujuan lain yang sering dikaitkan dengan FP:
Kekekalan
Josh Bloch's Item 13 adalah "Prefer Immutability." Meskipun pembicaraan sampah umum bertentangan, OOP dapat dilakukan dengan objek yang tidak dapat diubah, dan melakukan hal itu membuatnya jauh lebih baik. Misalnya, String di Java tidak dapat diubah. StringBuffer, OTOH harus bisa berubah untuk membangun String yang tidak berubah. Beberapa tugas, seperti bekerja dengan buffer secara inheren membutuhkan kemampuan berubah-ubah.
Kemurnian
Setiap fungsi setidaknya harus dapat dipindah-pindahkan - jika Anda memberikan parameter input yang sama (dan seharusnya tidak memiliki input selain argumen aktualnya), ia harus menghasilkan output yang sama setiap kali tanpa menyebabkan "efek samping" seperti mengubah keadaan global, melakukan / O, atau melempar pengecualian.
Dikatakan bahwa dalam Pemrograman Fungsional, "beberapa kejahatan biasanya diperlukan untuk menyelesaikan pekerjaan." Kesucian 100% umumnya bukan tujuan. Meminimalkan efek sampingnya.
Kesimpulan
Sungguh, dari semua ide di atas, ketidakmampuan telah menjadi kemenangan tunggal terbesar dalam hal aplikasi praktis untuk menyederhanakan kode saya - apakah OOP, atau FP. Melewati fungsi ke iterator adalah kemenangan terbesar kedua. The Java 8 Lambdas dokumentasi memiliki penjelasan terbaik mengapa. Rekursi sangat bagus untuk memproses pohon. Kemalasan memungkinkan Anda bekerja dengan koleksi tak terbatas.
Jika Anda menyukai JVM, saya sarankan Anda melihat Scala dan Clojure. Keduanya adalah interpretasi mendalam tentang Pemrograman Fungsional. Scala adalah tipe-aman dengan sintaksis mirip-C, meskipun ia benar-benar memiliki banyak sintaksis yang sama dengan Haskell seperti halnya dengan C. Clojure bukan tipe-aman dan itu adalah Lisp. Saya baru-baru ini memposting perbandingan Java, Scala dan Clojure sehubungan dengan satu masalah refactoring tertentu. Perbandingan Logan Campbell menggunakan Game of Life termasuk Haskell dan juga Clojure yang diketik.
PS
Jimmy Hoffa menunjukkan bahwa kelas Bus saya bisa berubah. Daripada memperbaiki yang asli, saya pikir ini akan menunjukkan jenis refactoring pertanyaan ini. Ini dapat diperbaiki dengan membuat setiap metode pada Bus sebuah pabrik untuk menghasilkan Bus baru, setiap metode pada Penumpang sebuah pabrik untuk menghasilkan Penumpang baru. Jadi saya telah menambahkan jenis kembali ke segala sesuatu yang berarti saya akan menyalin java.util.function.Function Java 8 bukan antarmuka konsumen:
Kemudian di Bus:
Akhirnya, objek fungsi anonim mengembalikan keadaan yang dimodifikasi (bus baru dengan penumpang baru). Ini mengasumsikan bahwa p.debit () sekarang mengembalikan Penumpang kekal baru dengan lebih sedikit uang daripada yang asli:
Semoga sekarang Anda dapat membuat keputusan sendiri tentang seberapa fungsional Anda ingin membuat bahasa imperatif Anda, dan memutuskan apakah akan lebih baik untuk mendesain ulang proyek Anda menggunakan bahasa fungsional. Di Scala atau Clojure, koleksi dan API lainnya dirancang untuk memudahkan pemrograman fungsional. Keduanya memiliki interop Java yang sangat bagus, sehingga Anda dapat mencampur dan mencocokkan bahasa. Bahkan, untuk interoperabilitas Java, Scala mengkompilasi fungsi kelas pertamanya ke kelas anonim yang hampir kompatibel dengan antarmuka fungsional Java 8. Anda dapat membaca tentang detail di Scala in Depth sect. 1.3.2 .
sumber
Saya punya pengalaman pribadi "menyelesaikan" ini. Pada akhirnya, saya tidak menemukan sesuatu yang murni fungsional, tetapi saya menemukan sesuatu yang saya sukai. Begini cara saya melakukannya:
x
, buatlah agar metode tersebut dilewatkan danx
bukannya meneleponthis.x
.x.methodThatModifiesTheFooVar()
menjadifooFn(x.foo)
map
,reduce
,filter
, dllSaya tidak bisa menyingkirkan keadaan yang bisa berubah. Itu terlalu non-idiomatis dalam bahasa saya (JavaScript). Tetapi, dengan membuat semua status masuk dan / atau dikembalikan, setiap fungsi dimungkinkan untuk diuji. Ini berbeda dari OOP di mana pengaturan negara akan memakan waktu terlalu lama atau pemisahan dependensi sering memerlukan modifikasi kode produksi terlebih dahulu.
Juga, saya bisa saja salah tentang definisi, tetapi saya pikir fungsi saya secara transparan transparan: Fungsi saya akan memiliki efek yang sama dengan input yang sama.
Edit
Seperti yang Anda lihat di sini , tidak mungkin membuat objek yang benar-benar tidak dapat diubah dalam JavaScript. Jika Anda rajin dan mengendalikan siapa yang memanggil kode Anda, Anda dapat melakukannya dengan selalu membuat objek baru alih-alih memutasikan yang sekarang. Itu tidak sepadan dengan usaha saya.
Tetapi jika Anda menggunakan Java, Anda dapat menggunakan teknik ini untuk membuat kelas Anda tidak berubah.
sumber
Saya tidak berpikir itu benar-benar mungkin untuk memperbaiki program sepenuhnya - Anda harus mendesain ulang dan mengimplementasikannya dalam paradigma yang benar.
Saya telah melihat kode refactoring didefinisikan sebagai "teknik disiplin untuk merestrukturisasi tubuh kode yang ada, mengubah struktur internalnya tanpa mengubah perilaku eksternalnya".
Anda dapat membuat hal-hal tertentu lebih fungsional, tetapi pada intinya Anda masih memiliki program berorientasi objek. Anda tidak bisa hanya mengubah sedikit demi sedikit untuk menyesuaikannya dengan paradigma yang berbeda.
sumber
Saya pikir seri artikel ini persis seperti yang Anda inginkan:
Retrogames Murni Fungsional
http://prog21.dadgum.com/23.html Bagian 1
http://prog21.dadgum.com/24.html Bagian 2
http://prog21.dadgum.com/25.html Bagian 3
http://prog21.dadgum.com/26.html Bagian 4
http://prog21.dadgum.com/37.html Tindak lanjut
Ringkasannya adalah:
Penulis menyarankan loop utama dengan efek samping (efek samping harus terjadi di suatu tempat, kan?) Dan sebagian besar fungsi mengembalikan catatan kecil yang tidak dapat diubah yang merinci bagaimana mereka mengubah keadaan permainan.
Tentu saja ketika menulis program dunia nyata Anda akan mencampur dan mencocokkan beberapa gaya pemrograman, menggunakan masing-masing gaya yang paling membantu. Namun itu adalah pengalaman belajar yang baik untuk mencoba menulis sebuah program dengan cara yang paling fungsional / tidak berubah dan juga menulisnya dengan cara yang paling spaghetti, hanya menggunakan variabel global :-) (lakukan sebagai percobaan, bukan dalam produksi, silakan)
sumber
Anda mungkin harus mengubah semua kode Anda ke dalam karena OOP dan FP memiliki dua pendekatan yang berlawanan untuk mengatur kode.
OOP mengatur kode seputar tipe (kelas): kelas yang berbeda dapat mengimplementasikan operasi yang sama (metode dengan tanda tangan yang sama). Sebagai hasilnya, OOP lebih tepat ketika rangkaian operasi tidak banyak berubah sementara tipe baru dapat ditambahkan sangat sering. Sebagai contoh, pertimbangkan sebuah perpustakaan GUI di mana setiap widget memiliki seperangkat tetap metode (
hide()
,show()
,paint()
,move()
, dan sebagainya) tapi widget baru bisa ditambahkan sebagai perpustakaan diperpanjang. Dalam OOP mudah untuk menambahkan tipe baru (untuk antarmuka yang diberikan): Anda hanya perlu menambahkan kelas baru dan mengimplementasikan semua metode (perubahan kode lokal). Di sisi lain, menambahkan operasi (metode) baru ke antarmuka mungkin memerlukan perubahan semua kelas yang mengimplementasikan antarmuka itu (meskipun warisan dapat mengurangi jumlah pekerjaan).FP mengatur kode di sekitar operasi (fungsi): setiap fungsi mengimplementasikan beberapa operasi yang dapat memperlakukan berbagai jenis dengan cara yang berbeda. Ini biasanya dicapai dengan mengirimkan jenis melalui pencocokan pola atau mekanisme lainnya. Sebagai konsekuensinya, FP lebih sesuai ketika rangkaian tipe stabil dan operasi baru ditambahkan lebih sering. Ambil contoh satu set tetap format gambar (GIF, JPEG, dll) dan beberapa algoritma yang ingin Anda terapkan. Setiap algoritma dapat diimplementasikan oleh fungsi yang berperilaku berbeda sesuai dengan jenis gambar. Menambahkan algoritma baru itu mudah karena Anda hanya perlu mengimplementasikan fungsi baru (perubahan kode lokal). Menambahkan format (tipe) baru memerlukan modifikasi semua fungsi yang telah Anda implementasikan sejauh ini untuk mendukungnya (perubahan non-lokal).
Intinya: OOP dan FP pada dasarnya berbeda dalam cara mereka mengatur kode, dan mengubah desain OOP menjadi desain FP akan melibatkan perubahan semua kode Anda untuk mencerminkan hal ini. Ini bisa menjadi latihan yang menarik. Lihat juga catatan kuliah ini untuk buku SICP yang dikutip oleh mikemay, khususnya slide 13.1.5 hingga 13.1.10.
sumber