API dan pemrograman fungsional

15

Dari paparan saya (memang terbatas) untuk bahasa pemrograman fungsional, seperti Clojure, tampaknya enkapsulasi data memiliki peran yang kurang penting. Biasanya berbagai jenis asli seperti peta atau set adalah mata uang yang dipilih untuk mewakili data, lebih dari objek. Selanjutnya, data itu umumnya tidak dapat diubah.

Misalnya, inilah salah satu kutipan terkenal dari Rich Hickey of Clojure fame, dalam sebuah wawancara tentang masalah ini :

Fogus: Mengikuti gagasan itu — beberapa orang dikejutkan oleh fakta bahwa Clojure tidak terlibat dalam enkapsulasi penyembunyian data pada tipenya. Mengapa Anda memutuskan untuk tidak lagi menyembunyikan data?

Hickey: Mari kita perjelas bahwa Clojure sangat menekankan pemrograman pada abstraksi. Pada titik tertentu, seseorang akan perlu memiliki akses ke data. Dan jika Anda memiliki gagasan "pribadi", Anda perlu gagasan tentang hak istimewa dan kepercayaan yang sesuai. Dan itu menambah satu ton kompleksitas dan sedikit nilai, menciptakan kekakuan dalam suatu sistem, dan seringkali memaksa hal-hal untuk hidup di tempat yang tidak seharusnya. Ini merupakan tambahan dari kehilangan lainnya yang terjadi ketika informasi sederhana dimasukkan ke dalam kelas. Sejauh data tidak dapat diubah, ada sedikit bahaya yang dapat terjadi karena menyediakan akses, selain dari itu seseorang dapat bergantung pada sesuatu yang mungkin berubah. Baiklah, orang-orang melakukan itu sepanjang waktu di kehidupan nyata, dan ketika segalanya berubah, mereka beradaptasi. Dan jika mereka rasional, mereka tahu kapan mereka membuat keputusan berdasarkan sesuatu yang dapat berubah yang mungkin mereka perlu adaptasi di masa depan. Jadi, ini adalah keputusan manajemen risiko, yang menurut saya programmer harus bebas untuk membuat. Jika orang tidak memiliki kepekaan terhadap keinginan untuk memprogram abstraksi dan untuk berhati-hati dalam mengawinkan detail implementasi, maka mereka tidak akan pernah menjadi programmer yang baik.

Datang dari dunia OO, ini tampaknya menyulitkan beberapa prinsip yang diabadikan yang telah saya pelajari selama bertahun-tahun. Ini termasuk Menyembunyikan Informasi, Hukum Demeter dan Prinsip Akses Seragam, untuk beberapa nama. Utas umum adalah enkapsulasi memungkinkan kita untuk mendefinisikan API agar orang lain tahu apa yang harus dan tidak boleh mereka sentuh. Intinya, membuat kontrak yang memungkinkan pengelola beberapa kode untuk secara bebas melakukan perubahan dan perbaikan tanpa khawatir tentang bagaimana bug dapat dimasukkan ke dalam kode konsumen (prinsip Terbuka / Tertutup). Ini juga menyediakan antarmuka yang bersih, dikuratori untuk programmer lain untuk mengetahui alat apa yang dapat mereka gunakan untuk mendapatkan atau membangun data itu.

Ketika data diizinkan untuk diakses secara langsung, kontrak API itu rusak dan semua manfaat enkapsulasi itu tampaknya hilang. Selain itu, data yang benar-benar tidak dapat diubah tampaknya membuat keliling struktur spesifik domain (objek, struct, catatan) jauh kurang berguna dalam arti mewakili suatu keadaan dan serangkaian tindakan yang dapat dilakukan pada keadaan itu.

Bagaimana basis kode fungsional mengatasi masalah ini yang tampaknya muncul ketika ukuran basis kode tumbuh sangat besar sehingga API perlu didefinisikan dan banyak pengembang yang terlibat dalam bekerja dengan bagian-bagian tertentu dari sistem? Apakah ada contoh situasi ini yang tersedia yang menunjukkan bagaimana ini ditangani dalam jenis basis kode ini?

jameslk
sumber
2
Anda dapat mendefinisikan antarmuka formal tanpa gagasan objek. Cukup buat fungsi antarmuka yang mendokumentasikannya. Jangan berikan dokumentasi untuk detail implementasi. Anda baru saja membuat antarmuka.
Scara95
@ Scara95 Bukankah itu berarti saya harus melakukan pekerjaan untuk mengimplementasikan kode untuk antarmuka dan menulis dokumentasi yang cukup tentang hal itu untuk memperingatkan konsumen apa yang harus dilakukan dan apa yang tidak boleh dilakukan? Bagaimana jika kode berubah dan dokumentasinya menjadi basi? Saya biasanya lebih suka kode dokumentasi diri karena alasan ini.
jameslk
Anda tetap harus mendokumentasikan antarmuka.
Scara95
3
Also, strictly immutable data seems to make passing around domain-specific structures (objects, structs, records) much less useful in the sense of representing a state and the set of actions that can be performed on that state.Tidak juga. Satu-satunya hal yang berubah adalah bahwa perubahan tersebut berakhir pada objek baru. Ini adalah kemenangan besar dalam hal pertimbangan tentang kode; melewati benda-benda yang bisa berubah-ubah berarti harus melacak siapa yang mungkin memutasi mereka, suatu masalah yang meningkat dengan ukuran kode.
Doval

Jawaban:

10

Pertama-tama, saya akan komentar kedua Sebastian tentang apa yang benar fungsional, apa yang mengetik dinamis. Secara umum, Clojure adalah salah satu citarasa bahasa fungsional dan komunitas, dan Anda tidak boleh terlalu menyamaratakannya. Saya akan membuat beberapa komentar dari lebih dari perspektif ML / Haskell.

Seperti yang disebutkan Basile, konsep kontrol akses memang ada dalam ML / Haskell, dan sering digunakan. "Anjak piutang" sedikit berbeda dari bahasa OOP konvensional; dalam OOP konsep kelas memainkan secara simultan peran tipe dan modul , sedangkan bahasa fungsional (dan prosedur tradisional) memperlakukan ini secara ortogonal.

Poin lain adalah bahwa ML / Haskell sangat berat pada obat generik dengan tipe erasure, dan ini dapat digunakan untuk memberikan rasa berbeda "penyembunyian informasi" daripada enkapsulasi OOP. Ketika suatu komponen hanya mengetahui jenis item data sebagai parameter tipe, komponen itu dapat dengan aman menyerahkan nilai dari tipe itu, namun itu akan dicegah dari melakukan banyak hal dengan mereka karena ia tidak tahu dan tidak dapat mengetahui tipe konkretnya. (tidak ada instanceofcasting universal atau runtime dalam bahasa ini). Entri blog ini adalah salah satu contoh pengantar favorit saya untuk teknik ini.

Berikutnya: di dunia FP sangat umum untuk menggunakan struktur data transparan sebagai antarmuka untuk komponen yang tidak jelas / terbungkus. Sebagai contoh, pola interpreter sangat umum di FP, di mana struktur data digunakan sebagai pohon sintaks yang menggambarkan logika, dan diumpankan ke kode yang "mengeksekusi" mereka. Nyatakan, dengan benar, kemudian ada sesaat ketika interpreter berjalan yang menggunakan struktur data. Implementasi interpreter juga dapat berubah selama masih berkomunikasi dengan klien dalam hal tipe data yang sama.

Terakhir dan terpanjang: enkapsulasi / penyembunyian informasi adalah teknik , bukan tujuan. Mari kita berpikir sedikit tentang apa yang disediakannya. Enkapsulasi adalah teknik untuk merekonsiliasi kontrak dan implementasi unit perangkat lunak. Situasi khasnya adalah ini: implementasi sistem mengakui nilai-nilai atau menyatakan bahwa, menurut kontraknya, seharusnya tidak ada.

Setelah Anda melihatnya dengan cara ini, kami dapat menunjukkan bahwa FP menyediakan, selain enkapsulasi, sejumlah alat tambahan yang dapat digunakan untuk tujuan yang sama:

  1. Kekekalan sebagai standar yang meresap. Anda dapat memberikan nilai data transparan ke kode pihak ketiga. Mereka tidak dapat memodifikasinya dan menempatkannya dalam status tidak valid. (Jawaban Karl membuat poin ini.)
  2. Sistem tipe canggih dengan tipe data aljabar yang memungkinkan Anda untuk mengontrol struktur tipe Anda dengan baik, tanpa menulis banyak kode. Dengan secara bijaksana menggunakan fasilitas ini, Anda sering dapat merancang jenis di mana "kondisi buruk" tidak mungkin. (Slogan: "Jadikan negara ilegal tidak terwakili." ) Alih-alih menggunakan enkapsulasi untuk secara tidak langsung mengendalikan set negara yang dapat diterima dalam suatu kelas, saya lebih baik memberi tahu kompiler apa saja itu dan meminta jaminan bagi saya!
  3. Pola juru bahasa, sebagaimana telah disebutkan. Salah satu kunci untuk merancang jenis pohon sintaksis abstrak yang baik adalah dengan:
    • Coba dan desain tipe data pohon sintaksis abstrak sehingga semua nilai "valid."
    • Jika gagal, buat penerjemah secara eksplisit mendeteksi kombinasi yang tidak valid dan dengan tegas menolaknya.

Seri F # "Merancang dengan tipe" ini menghasilkan bacaan yang lumayan tentang beberapa topik ini, khususnya # 2. (Dari situlah tautan "membuat negara tidak terwakili" dari atas). Jika Anda perhatikan dengan seksama, Anda akan memperhatikan bahwa pada bagian kedua mereka mendemonstrasikan bagaimana menggunakan enkapsulasi untuk menyembunyikan konstruktor dan mencegah klien membuat instance yang tidak valid. Seperti yang saya katakan di atas, ini adalah bagian dari toolkit!

sakundim
sumber
9

Saya benar-benar tidak bisa melebih-lebihkan sejauh mana mutabilitas menyebabkan masalah dalam perangkat lunak. Banyak praktik yang dimasukkan ke kepala kita sebagai kompensasi atas masalah yang menyebabkan ketidakstabilan. Ketika Anda menghilangkan mutabilitas, Anda tidak perlu banyak berlatih seperti itu.

Ketika Anda memiliki ketidakmampuan, Anda tahu struktur data Anda tidak akan berubah dari bawah Anda secara tak terduga selama runtime, sehingga Anda dapat membuat struktur data turunan Anda sendiri untuk Anda gunakan saat Anda menambahkan fitur ke program Anda. Struktur data asli tidak perlu tahu apa pun tentang struktur data turunan ini.

Ini berarti struktur data dasar Anda cenderung sangat stabil. Struktur data baru semacam diturunkan dari sekitar tepi sesuai kebutuhan. Sangat sulit untuk dijelaskan sampai Anda telah melakukan program fungsional yang signifikan. Anda baru saja semakin peduli dengan privasi, dan semakin memikirkan untuk membuat struktur data publik generik yang lebih tahan lama.

Karl Bielefeldt
sumber
Satu hal yang ingin saya tambahkan adalah bahwa variabel yang tidak dapat diubah menyebabkan pemrogram tetap menggunakan struktur data yang tersebar dan tersebar, jika ada struktur sama sekali. Semua data disusun untuk membuat grup logika, agar mudah ditemukan dan dilintasi, bukan untuk transportasi. Ini adalah perkembangan logika yang akan Anda buat setelah Anda melakukan pemrograman fungsional yang cukup.
Xephon
8

Kecenderungan Clojure untuk hanya menggunakan hash dan primitif bukan, menurut pendapat saya, bagian dari warisan fungsionalnya, tetapi bagian dari warisan dinamisnya. Saya telah melihat kecenderungan serupa di Python dan Ruby (keduanya berorientasi objek, penting dan dinamis, meskipun keduanya memiliki dukungan yang cukup baik untuk fungsi tingkat tinggi), tetapi tidak di, katakanlah, Haskell (yang diketik secara statis, tetapi murni fungsional , dengan konstruksi khusus yang diperlukan untuk menghindari ketidakberdayaan).

Jadi pertanyaan yang perlu Anda tanyakan bukan, bagaimana bahasa fungsional menangani API besar, tetapi bagaimana bahasa dinamis melakukannya. Jawabannya adalah: dokumentasi yang baik dan banyak dan banyak unit test. Untungnya, bahasa dinamis modern biasanya datang dengan dukungan yang sangat baik untuk keduanya; misalnya, baik Python dan Clojure memiliki cara menyematkan dokumentasi dalam kode itu sendiri, bukan hanya komentar.

Sebastian Redl
sumber
Tentang bahasa fungsional yang diketik secara statis (murni), tidak ada cara (sederhana) untuk membawa fungsi dengan tipe data seperti dalam pemrograman OO. Jadi dokumentasi itu penting. Intinya adalah Anda tidak perlu dukungan bahasa untuk mendefinisikan antarmuka.
Scara95
5
@ Scara95 Bisakah Anda menguraikan apa yang Anda maksud dengan "membawa fungsi dengan tipe data"?
Sebastian Redl
6

Beberapa bahasa fungsional memberikan kemampuan untuk merangkum atau menyembunyikan detail implementasi dalam tipe dan modul data abstrak .

Sebagai contoh, OCaml memiliki modul yang didefinisikan oleh kumpulan tipe dan nilai abstrak bernama (terutama fungsi yang beroperasi pada tipe abstrak ini). Jadi dalam arti tertentu, modul Ocaml adalah reifing API. Ocaml juga memiliki functors, yang mengubah beberapa modul menjadi modul lain, sehingga menyediakan pemrograman generik. Jadi modul adalah komposisi.

Basile Starynkevitch
sumber