Penjelasan sederhana tentang protokol clojure

131

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?

appshare.co
sumber
7
Clojure 1.2 Protokol dalam 27 menit: vimeo.com/11236603
miku
3
Analogi yang sangat dekat dengan Protokol adalah Traits (mixins) dalam Scala: stackoverflow.com/questions/4508125/…
Vasil Remeniuk

Jawaban:

284

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,, casepencocokan 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 StackProtokol terdiri dari baik suatu pushdan popfungsi 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: Seqmisalnya Protokol. Tetapi sampai 1.2, Anda tidak dapat menulis Protokol di Clojure, Anda harus menulisnya dalam bahasa host.

Jörg W Mittag
sumber
Terima kasih atas jawaban yang saksama, tetapi dapatkah Anda menjelaskan maksud Anda tentang Ruby. Saya kira kemampuan untuk (kembali) mendefinisikan metode dari setiap kelas (misalnya String, Fixnum) di Ruby adalah analogi dengan defprotocol Clojure.
defhlt
3
Artikel yang bagus tentang Masalah Ekspresi dan protokol clojure - ibm.com/developerworks/library/j-clojure-protocols
navgeet
Maaf untuk mengirim komentar pada jawaban yang lama tapi bisakah Anda menjelaskan mengapa ekstensi dan antarmuka (C # / Java) bukan solusi yang baik untuk Masalah Ekspresi?
Onorio Catenacci
Java tidak memiliki ekstensi dalam arti istilah tersebut digunakan di sini.
user100464
Ruby memiliki penyempurnaan yang membuat tambalan monyet usang.
Marcin Bilski
64

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
mikera
sumber
1
> seperti parameter "this" yang tersirat dalam bahasa berorientasi objek, saya perhatikan bahwa var yang diteruskan ke fungsi protokol sering juga disebut thisdalam kode Clojure.
Kris