Bagaimana bahasa yang murni fungsional menangani modularitas?

23

Saya berasal dari latar belakang berorientasi objek di mana saya telah belajar bahwa kelas atau setidaknya dapat digunakan untuk membuat lapisan abstraksi yang memungkinkan daur ulang kode yang mudah yang kemudian dapat digunakan untuk membuat objek atau digunakan dalam warisan.

Seperti misalnya saya dapat memiliki kelas hewan dan kemudian dari itu mewarisi kucing dan anjing dan sedemikian rupa sehingga semuanya mewarisi banyak sifat yang sama, dan dari sub-kelas tersebut saya kemudian dapat membuat objek yang dapat menentukan jenis hewan atau bahkan nama. itu.
Atau saya bisa menggunakan kelas untuk menentukan beberapa instance dari kode yang sama yang menangani atau berisi hal-hal yang sedikit berbeda; seperti node dalam pohon pencarian atau beberapa koneksi database yang berbeda dan apa yang tidak.

Saya baru saja pindah ke pemrograman fungsional, jadi saya mulai bertanya-tanya:
Bagaimana bahasa fungsional murni menangani hal-hal seperti itu? Yaitu, bahasa tanpa konsep kelas dan objek.

Kopi Listrik
sumber
1
Menurut Anda mengapa fungsional itu tidak berarti kelas? Beberapa kelas pertama berasal dari LISP - CLOS Lihatlah clojure namespaces dan tipe atau modul dan haskell .
Saya merujuk ke bahasa fungsional yang TIDAK memiliki kelas, saya sangat menyadari beberapa yang DO
Electric Coffee
1
Caml sebagai contoh, bahasa saudara OCaml menambahkan objek, tetapi Caml sendiri tidak memilikinya.
Kopi Listrik
12
Istilah "murni fungsional" mengacu pada bahasa fungsional yang mempertahankan transparansi referensial dan tidak terkait dengan apakah bahasa tersebut memiliki fitur berorientasi objek atau tidak.
sepp2k
2
Kue itu bohong, penggunaan kembali kode di OO jauh lebih sulit daripada di FP. Untuk semua yang telah diklaim OO menggunakan kembali kode selama bertahun-tahun, saya telah melihatnya mengikuti minimal kali. (jangan ragu untuk mengatakan bahwa saya melakukan kesalahan, saya merasa nyaman dengan seberapa baik saya menulis kode OO karena harus merancang dan memelihara sistem OO selama bertahun-tahun, saya tahu kualitas hasil saya sendiri)
Jimmy Hoffa

Jawaban:

19

Banyak bahasa fungsional memiliki sistem modul. (Banyak bahasa berorientasi objek juga, by the way.) Tetapi bahkan tanpa adanya satu, Anda dapat menggunakan fungsi sebagai modul.

JavaScript adalah contoh yang bagus. Dalam JavaScript, fungsi digunakan baik untuk mengimplementasikan modul dan bahkan enkapsulasi berorientasi objek. Dalam Skema, yang merupakan inspirasi utama untuk JavaScript, hanya ada fungsi. Fungsi digunakan untuk mengimplementasikan hampir semua hal: objek, modul (disebut unit dalam Racket), bahkan struktur data.

OTOH, Haskell dan keluarga ML memiliki sistem modul eksplisit.

Orientasi Objek adalah tentang abstraksi data. Itu dia. Modularitas, Warisan, Polimorfisme, bahkan keadaan yang bisa berubah adalah masalah ortogonal.

Jörg W Mittag
sumber
8
Bisakah Anda menjelaskan bagaimana hal-hal itu bekerja sedikit lebih detail dalam kaitannya dengan oop? Daripada hanya menyatakan bahwa konsep itu ada ...
Electric Coffee
Sidenote - Modul dan unit adalah dua konstruksi berbeda di Racket - modul dapat dibandingkan dengan ruang nama, dan unit adalah semacam separuh jalan antara ruang nama dan antarmuka OO. Dokumen-dokumen tersebut menjelaskan lebih jauh perbedaan-perbedaan
Jack
@ Jack: Saya tidak tahu bahwa Racket juga memiliki konsep yang disebut module. Saya pikir sangat disayangkan bahwa Racket memiliki konsep yang disebut moduleyang bukan modul, dan konsep yang merupakan modul tetapi tidak disebut module. Ngomong-ngomong, Anda menulis: "unit semacam setengah antara ruang nama dan antarmuka OO". Nah, bukankah itu semacam definisi modul itu?
Jörg W Mittag
Modul dan unit adalah kelompok nama yang terikat dengan nilai. Modul dapat memiliki dependensi pada set binding spesifik lainnya , sementara unit dapat memiliki dependensi pada beberapa set binding umum yang harus disediakan oleh kode lain yang menggunakan unit. Unit diparameterisasi melalui binding, modul tidak. Modul yang tergantung pada penjilidan mapdan unit yang tergantung pada penjilidan mapberbeda karena modul harus merujuk pada mappenjilidan tertentu , seperti dari penjilidan racket/base, sementara pengguna unit yang berbeda dapat memberikan definisi berbeda mapuntuk unit tersebut.
Jack
4

Sepertinya Anda mengajukan dua pertanyaan: "Bagaimana Anda bisa mencapai modularitas dalam bahasa fungsional?" yang telah dibahas dalam jawaban lain dan "bagaimana Anda bisa membuat abstraksi dalam bahasa fungsional?" yang akan saya jawab.

Dalam bahasa OO, Anda cenderung berkonsentrasi pada kata benda, "seekor binatang", "server surat", "garpu kebunnya", dll. Bahasa fungsional, sebaliknya, menekankan kata kerja, "berjalan", "untuk mengambil surat" , "untuk mendorong", dll.

Maka, tidak mengherankan bahwa abstraksi dalam bahasa fungsional cenderung lebih dari kata kerja atau operasi daripada hal-hal. Salah satu contoh yang selalu saya raih ketika saya mencoba menjelaskan ini adalah parsing. Dalam bahasa fungsional, cara yang baik untuk menulis parser adalah dengan menentukan tata bahasa dan kemudian menafsirkannya. Penerjemah menciptakan abstraksi atas proses penguraian.

Contoh konkret lain dari ini adalah proyek yang saya kerjakan belum lama ini. Saya sedang menulis database di Haskell. Saya punya satu 'bahasa tertanam' untuk menentukan operasi di level terendah; misalnya, itu memungkinkan saya untuk menulis dan membaca sesuatu dari media penyimpanan. Saya memiliki 'bahasa tertanam' lain yang terpisah untuk menentukan operasi di level tertinggi. Lalu saya punya, apa yang pada dasarnya adalah juru bahasa, untuk mengubah operasi dari level yang lebih tinggi ke level yang lebih rendah.

Ini adalah bentuk abstraksi yang sangat umum, tetapi ini bukan satu-satunya yang tersedia dalam bahasa fungsional.

dan_waterworth
sumber
4

Meskipun "pemrograman fungsional" tidak membawa implikasi yang luas untuk masalah modularitas, bahasa-bahasa tertentu membahas pemrograman dalam cara yang berbeda. Penggunaan ulang dan abstraksi kode berinteraksi dalam semakin sedikit Anda mengekspos semakin sulit untuk menggunakan kembali kode. Mengesampingkan abstraksi, saya akan membahas dua masalah penggunaan kembali.

Bahasa OOP yang diketik secara statis biasanya menggunakan subtyping nominal, yang berarti bahwa kode yang dirancang untuk kelas / modul / antarmuka A hanya dapat menangani kelas / modul / antarmuka B ketika B secara eksplisit menyebutkan A. Bahasa dalam keluarga pemrograman fungsional terutama menggunakan subtyping struktural, yang berarti kode yang dirancang untuk A dapat menangani B setiap kali B memiliki semua metode dan / atau bidang A. B dapat dibuat oleh tim yang berbeda sebelum ada kebutuhan untuk kelas / antarmuka yang lebih umum A. Misalnya dalam OCaml, subtyping struktural berlaku untuk sistem modul, sistem objek mirip OOP, dan jenis varian polimorfiknya yang cukup unik.

Perbedaan paling menonjol antara OOP dan FP wrt. modularitas adalah bahwa "unit" default dalam bundel OOP bersama sebagai objek berbagai operasi pada nilai kasus yang sama, sedangkan "unit" default dalam bundel FP bersama sebagai fungsi operasi yang sama untuk berbagai kasus nilai. Dalam FP masih sangat mudah untuk menggabungkan operasi bersama, misalnya sebagai modul. (BTW, baik Haskell maupun F # tidak memiliki sistem modul keluarga lengkap.) Masalah Ekspresiadalah tugas menambahkan secara bertahap kedua operasi baru yang bekerja pada semua nilai (mis. melampirkan metode baru ke objek yang ada), dan kasus nilai baru yang harus didukung oleh semua operasi (misalnya menambahkan kelas baru dengan antarmuka yang sama). Seperti dibahas dalam ceramah Ralf Laemmel pertama di bawah ini (yang memiliki contoh luas dalam C #), menambahkan operasi baru bermasalah dalam bahasa OOP.

Kombinasi OOP dan FP dalam Scala mungkin menjadikannya salah satu bahasa yang paling kuat wrt. modularitas. Tetapi OCaml masih merupakan bahasa favorit saya dan menurut pendapat pribadi saya, subyektif itu tidak kalah dengan Scala. Dua kuliah Ralf Laemmel di bawah ini membahas solusi untuk masalah ekspresi di Haskell. Saya pikir solusi ini, meskipun berfungsi dengan baik, membuatnya sulit untuk menggunakan data yang dihasilkan dengan polimorfisme parametrik. Memecahkan masalah ekspresi dengan varian polimorfik di OCaml, dijelaskan dalam artikel Jaques Garrigue yang ditautkan di bawah, tidak memiliki kekurangan ini. Saya juga menautkan ke bab buku teks yang membandingkan penggunaan modularitas non-OOP dan OOP di OCaml.

Di bawah ini adalah tautan spesifik Haskell dan OCaml yang berkembang pada Masalah Ekspresi :

lukstafi
sumber
2
maukah Anda menjelaskan lebih lanjut tentang apa yang dilakukan sumber daya ini dan mengapa Anda merekomendasikan ini sebagai jawaban atas pertanyaan yang diajukan? "Jawaban khusus tautan" tidak diterima di Stack Exchange
agas
2
Saya baru saja memberikan jawaban aktual dan bukan hanya tautan, sebagai hasil edit.
lukstafi
0

Sebenarnya, kode OO jauh lebih tidak dapat digunakan kembali, dan itu adalah desain. Gagasan di balik OOP adalah untuk membatasi operasi pada potongan data tertentu ke kode istimewa tertentu yang ada di kelas atau di tempat yang sesuai dalam hierarki warisan. Ini membatasi efek buruk dari mutabilitas. Jika struktur data berubah, hanya ada begitu banyak tempat dalam kode yang dapat bertanggung jawab.

Dengan kekekalan, Anda tidak peduli siapa yang dapat beroperasi pada struktur data apa pun, karena tidak ada yang dapat mengubah salinan data Anda. Ini membuat membuat fungsi-fungsi baru untuk bekerja pada struktur data yang ada jauh lebih mudah. Anda cukup membuat fungsi dan mengelompokkannya ke dalam modul yang tampaknya sesuai dari sudut pandang domain. Anda tidak perlu khawatir tentang di mana menempatkan mereka ke dalam hierarki warisan.

Jenis lain penggunaan kembali kode adalah menciptakan struktur data baru untuk bekerja pada fungsi yang ada. Ini ditangani dalam bahasa fungsional menggunakan fitur seperti generik dan kelas tipe. Sebagai contoh, kelas jenis Ord Haskell memungkinkan Anda untuk menggunakan sortfungsi pada jenis apa pun dengan sebuah Ordinstance. Contoh mudah dibuat jika belum ada.

Ambil Animalcontoh Anda , dan pertimbangkan menerapkan fitur pemberian makan. Implementasi OOP langsung adalah untuk mempertahankan koleksi Animalobjek, dan loop melalui semuanya, memanggil feedmetode pada masing-masing.

Namun, banyak hal menjadi rumit ketika Anda sampai ke detail. Suatu Animalobjek secara alami tahu jenis makanan apa yang dimakannya, dan berapa banyak yang dibutuhkan agar merasa kenyang. Itu tidak secara alami tahu di mana makanan disimpan dan berapa banyak tersedia, sehingga suatu FoodStoreobjek baru saja menjadi ketergantungan setiap Animal, baik sebagai bidang Animalobjek, atau diteruskan sebagai parameter feedmetode. Secara bergantian, untuk menjaga agar Animalkelas lebih kohesif, Anda mungkin pindah feed(animal)ke FoodStoreobjek, atau Anda dapat membuat kekejian dari kelas yang disebut AnimalFeederatau semacamnya.

Dalam FP, tidak ada kecenderungan untuk bidang Animaluntuk selalu tetap dikelompokkan bersama, yang memiliki beberapa implikasi menarik untuk dapat digunakan kembali. Katakanlah Anda memiliki daftar Animalcatatan, dengan bidang seperti name, species, location, food type, food amount, dll Anda juga memiliki daftar FoodStorecatatan dengan bidang-bidang seperti location, food type, dan food amount.

Langkah pertama dalam pemberian makanan mungkin adalah memetakan masing-masing daftar catatan tersebut ke daftar (food amount, food type)pasangan, dengan angka negatif untuk jumlah hewan. Anda kemudian dapat membuat fungsi untuk melakukan segala macam hal dengan pasangan ini, seperti jumlah jumlah masing-masing jenis makanan. Fungsi-fungsi ini bukan milik modul Animalatau FoodStoremodul, tetapi sangat dapat digunakan kembali oleh keduanya.

Anda berakhir dengan banyak fungsi yang melakukan hal-hal berguna dengan [(Num A, Eq B)]yang dapat digunakan kembali dan modular, tetapi Anda memiliki kesulitan mencari tahu di mana harus meletakkannya atau apa yang harus mereka sebut sebagai grup. Efeknya adalah bahwa modul FP lebih sulit untuk diklasifikasi, tetapi klasifikasinya jauh kurang penting.

Karl Bielefeldt
sumber
-1

Salah satu solusi populer, adalah memecah kode menjadi modul, berikut ini cara melakukannya dalam JavaScript:

    media.podcast = (function(name) {
    var fileExtension = 'mp3';        

     function determineFileExtension() {
         console.log('File extension is of type ' + fileExtension);
     }

     return {
         download: function(episode) {
            console.log('Downloading ' + episode + ' of ' + name);
            determineFileExtension();
        }
    }    
}('Astronomy podcast'));

The artikel lengkap menjelaskan pola ini dalam JavaScript , selain itu ada beberapa cara lain untuk menentukan sebuah modul, seperti RequireJS , CommonJS , Google Penutupan. Contoh lain adalah Erlang, di mana Anda memiliki modul dan perilaku yang menegakkan API dan pola, memainkan peran yang sama seperti Antarmuka di OOP.

David Sergey
sumber