Saya mencoba memahami protokol clojure dan masalah apa yang seharusnya mereka pecahkan. Apakah ada yang punya penjelasan yang jelas tentang apa dan mengapa protokol clojure?
Saya mencoba memahami protokol clojure dan masalah apa yang seharusnya mereka pecahkan. Apakah ada yang punya penjelasan yang jelas tentang apa dan mengapa protokol clojure?
Jawaban:
Tujuan Protokol di Clojure adalah untuk memecahkan Masalah Ekspresi secara efisien.
Jadi, apa Masalah Ekspresi? Ini merujuk pada masalah dasar ekstensibilitas: program kami memanipulasi tipe data menggunakan operasi. Seiring program kami berkembang, kami perlu memperluasnya dengan tipe data baru dan operasi baru. Dan khususnya, kami ingin dapat menambahkan operasi baru yang bekerja dengan tipe data yang ada, dan kami ingin menambahkan tipe data baru yang bekerja dengan operasi yang ada. Dan kami ingin ini menjadi ekstensi sejati , yaitu kami tidak ingin mengubah yang adaprogram, kami ingin menghormati abstraksi yang ada, kami ingin ekstensi kami menjadi modul yang terpisah, dalam ruang nama yang terpisah, dikompilasi secara terpisah, dikerahkan secara terpisah, ketik secara terpisah diperiksa. Kami ingin mereka menjadi tipe-aman. [Catatan: tidak semua ini masuk akal dalam semua bahasa. Tapi, misalnya, tujuan untuk membuat mereka aman-mengetik masuk akal bahkan dalam bahasa seperti Clojure. Hanya karena kita tidak dapat secara statis memeriksa keamanan jenis tidak berarti kita ingin kode kita rusak secara acak, kan?]
Masalah Ekspresi adalah, bagaimana Anda benar-benar memberikan ekstensibilitas dalam bahasa?
Ternyata untuk implementasi naif khas pemrograman prosedural dan / atau fungsional, sangat mudah untuk menambahkan operasi baru (prosedur, fungsi), tetapi sangat sulit untuk menambahkan tipe data baru, karena pada dasarnya operasi bekerja dengan tipe data menggunakan beberapa semacam diskriminasi kasus ( switch
,, case
pencocokan pola) dan Anda perlu menambahkan kasus baru ke dalamnya, yaitu memodifikasi kode yang ada:
func print(node):
case node of:
AddOperator => print(node.left) + '+' + print(node.right)
NotOperator => '!' + print(node)
func eval(node):
case node of:
AddOperator => eval(node.left) + eval(node.right)
NotOperator => !eval(node)
Sekarang, jika Anda ingin menambahkan operasi baru, katakanlah, pengecekan tipe, itu mudah, tetapi jika Anda ingin menambahkan tipe simpul baru, Anda harus memodifikasi semua ekspresi pencocokan pola yang ada di semua operasi.
Dan untuk OO naif biasa, Anda memiliki masalah sebaliknya: mudah untuk menambahkan tipe data baru yang bekerja dengan operasi yang ada (baik dengan mewarisi atau menggantinya), tetapi sulit untuk menambahkan operasi baru, karena itu pada dasarnya berarti memodifikasi kelas / objek yang ada.
class AddOperator(left: Node, right: Node) < Node:
meth print:
left.print + '+' + right.print
meth eval
left.eval + right.eval
class NotOperator(expr: Node) < Node:
meth print:
'!' + expr.print
meth eval
!expr.eval
Di sini, menambahkan tipe simpul baru itu mudah, karena Anda mewarisi, menimpa, atau mengimplementasikan semua operasi yang diperlukan, tetapi menambahkan operasi baru itu sulit, karena Anda perlu menambahkannya ke semua kelas daun atau ke kelas dasar, sehingga memodifikasi yang sudah ada kode.
Beberapa bahasa memiliki beberapa konstruksi untuk menyelesaikan Masalah Ekspresi: Haskell memiliki typeclasses, Scala memiliki argumen implisit, Racket memiliki Unit, Go memiliki Antarmuka, CLOS dan Clojure memiliki Multimethods. Ada juga "solusi" yang mencoba menyelesaikannya, tetapi gagal dengan satu atau lain cara: Antarmuka dan Metode Ekstensi dalam C # dan Java, Monkeypatching di Ruby, Python, ECMAScript.
Perhatikan bahwa Clojure sebenarnya sudah memiliki mekanisme untuk menyelesaikan Masalah Ekspresi: Multimethods. Masalah yang OO miliki dengan EP adalah bahwa mereka menggabungkan operasi dan tipe bersama. Dengan Multimethods mereka terpisah. Masalah yang dimiliki FP adalah bahwa mereka menggabungkan operasi dan diskriminasi kasus bersama. Sekali lagi, dengan Multimethods mereka terpisah.
Jadi, mari kita bandingkan Protokol dengan Metode Multimetode, karena keduanya melakukan hal yang sama. Atau, dengan kata lain: Mengapa Protokol jika kita sudah memiliki Multimethods?
Hal utama yang ditawarkan Protokol daripada Metode Multimetode adalah Pengelompokan: Anda dapat mengelompokkan beberapa fungsi secara bersamaan dan mengatakan "3 fungsi ini bersama-sama membentuk Protokol Foo
". Anda tidak dapat melakukannya dengan Multimethods, mereka selalu berdiri sendiri. Misalnya, Anda bisa menyatakan bahwa Stack
Protokol terdiri dari baik suatu push
dan pop
fungsi bersama-sama .
Jadi, mengapa tidak menambahkan kemampuan untuk mengelompokkan Multimethods bersama? Ada alasan pragmatis murni, dan itulah sebabnya saya menggunakan kata "efisien" dalam kalimat pengantar saya: kinerja.
Clojure adalah bahasa yang di-host. Yakni dirancang khusus untuk dijalankan di atas platform bahasa lain . Dan ternyata hampir semua platform yang ingin Anda jalankan Clojure (JVM, CLI, ECMAScript, Objective-C) memiliki dukungan kinerja tinggi khusus untuk mengirim semata-mata pada jenis argumen pertama. Clojure Multimethods OTOH mengirimkan sifat sewenang-wenang dari semua argumen .
Jadi, Protokol membatasi Anda untuk mengirimkan hanya pada argumen pertama dan hanya pada jenisnya (atau sebagai kasus khusus aktif nil
).
Ini bukan batasan pada gagasan Protokol semata, ini adalah pilihan pragmatis untuk mendapatkan akses ke optimalisasi kinerja platform yang mendasarinya. Secara khusus, ini berarti bahwa Protokol memiliki pemetaan sepele untuk JVM / CLI Interfaces, yang membuatnya sangat cepat. Sebenarnya, cukup cepat untuk dapat menulis ulang bagian-bagian Clojure yang saat ini ditulis dalam Java atau C # di Clojure itu sendiri.
Clojure sebenarnya sudah memiliki Protokol sejak versi 1.0: Seq
misalnya Protokol. Tetapi sampai 1.2, Anda tidak dapat menulis Protokol di Clojure, Anda harus menulisnya dalam bahasa host.
Saya merasa paling membantu untuk memikirkan protokol yang secara konseptual mirip dengan "antarmuka" dalam bahasa berorientasi objek seperti Java. Protokol mendefinisikan seperangkat fungsi abstrak yang dapat diimplementasikan secara konkret untuk objek yang diberikan.
Sebuah contoh:
(defprotocol my-protocol
(foo [x]))
Menentukan protokol dengan satu fungsi yang disebut "foo" yang bekerja pada satu parameter "x".
Anda kemudian dapat membuat struktur data yang mengimplementasikan protokol, misalnya
(defrecord constant-foo [value]
my-protocol
(foo [x] value))
(def a (constant-foo. 7))
(foo a)
=> 7
Perhatikan bahwa di sini objek yang mengimplementasikan protokol dilewatkan sebagai parameter pertama x
- agak seperti implisit "ini" dalam bahasa berorientasi objek.
Salah satu fitur protokol yang sangat kuat dan berguna adalah Anda dapat memperluasnya ke objek bahkan jika objek tersebut awalnya tidak dirancang untuk mendukung protokol . mis. Anda dapat memperluas protokol di atas ke kelas java.lang.String jika Anda suka:
(extend-protocol my-protocol
java.lang.String
(foo [x] (.length x)))
(foo "Hello")
=> 5
this
dalam kode Clojure.