คำอธิบายง่ายๆของโปรโตคอล clojure


131

ฉันกำลังพยายามทำความเข้าใจโปรโตคอลของ clojure และปัญหาที่ควรแก้ไข ใครมีคำอธิบายที่ชัดเจนเกี่ยวกับโปรโตคอล clojure คืออะไร?


7
โปรโตคอล Clojure 1.2 ใน 27 นาที: vimeo.com/11236603
miku

3
การเปรียบเทียบที่ใกล้เคียงมากกับโปรโตคอลคือลักษณะ (มิกซ์อิน) ใน Scala: stackoverflow.com/questions/4508125/…
Vasil Remeniuk

คำตอบ:


284

วัตถุประสงค์ของโปรโตคอลใน Clojure คือการแก้ปัญหาการแสดงออกอย่างมีประสิทธิภาพ

ปัญหาการแสดงออกคืออะไร? หมายถึงปัญหาพื้นฐานของการขยาย: โปรแกรมของเราจัดการกับชนิดข้อมูลโดยใช้การดำเนินการ เมื่อโปรแกรมของเราพัฒนาขึ้นเราจำเป็นต้องขยายประเภทข้อมูลใหม่และการดำเนินการใหม่ ๆ และโดยเฉพาะอย่างยิ่งเราต้องการเพิ่มการดำเนินการใหม่ที่ใช้ได้กับประเภทข้อมูลที่มีอยู่และเราต้องการเพิ่มประเภทข้อมูลใหม่ที่ใช้ได้กับการดำเนินการที่มีอยู่ และเราต้องการให้นี่เป็นส่วนขยายที่แท้จริงกล่าวคือเราไม่ต้องการแก้ไขสิ่งที่มีอยู่โปรแกรมเราต้องการเคารพสิ่งที่เป็นนามธรรมที่มีอยู่เราต้องการให้ส่วนขยายของเราเป็นโมดูลแยกต่างหากในเนมสเปซแยกกันคอมไพล์แยกใช้งานแยกต่างหากตรวจสอบประเภทแยกกัน เราต้องการให้พวกเขาปลอดภัย [หมายเหตุ: ทั้งหมดนี้ไม่ได้มีเหตุผลในทุกภาษา แต่ตัวอย่างเช่นเป้าหมายที่จะให้พวกเขาพิมพ์ปลอดภัยนั้นสมเหตุสมผลแม้ในภาษาเช่น Clojure เพียงเพราะเราไม่สามารถตรวจสอบประเภทความปลอดภัยแบบคงที่ไม่ได้หมายความว่าเราต้องการให้โค้ดของเราแตกแบบสุ่มใช่ไหม]

ปัญหาของนิพจน์คือคุณให้ความสามารถในการขยายนี้ในภาษาได้อย่างไร?

ปรากฎว่าสำหรับการใช้งานขั้นตอนและ / หรือการเขียนโปรแกรมเชิงฟังก์ชันแบบไร้เดียงสาโดยทั่วไปการเพิ่มการดำเนินการใหม่ ๆ (ขั้นตอนฟังก์ชัน) ทำได้ง่ายมาก แต่ยากมากที่จะเพิ่มชนิดข้อมูลใหม่เนื่องจากโดยทั่วไปแล้วการดำเนินการจะทำงานกับประเภทข้อมูลโดยใช้บางส่วน ประเภทของการเลือกปฏิบัติกรณี ( switch,, การcaseจับคู่รูปแบบ) และคุณต้องเพิ่มกรณีและปัญหาใหม่เข้าไปเช่นแก้ไขรหัสที่มีอยู่:

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)

ตอนนี้ถ้าคุณต้องการเพิ่มการดำเนินการใหม่เช่นการตรวจสอบประเภทเป็นเรื่องง่าย แต่ถ้าคุณต้องการเพิ่มประเภทโหนดใหม่คุณต้องแก้ไขนิพจน์การจับคู่รูปแบบที่มีอยู่ทั้งหมดในการดำเนินการทั้งหมด

และสำหรับ OO ที่ไร้เดียงสาทั่วไปคุณมีปัญหาตรงข้ามอย่างแน่นอนนั่นคือง่ายที่จะเพิ่มชนิดข้อมูลใหม่ที่ทำงานกับการดำเนินการที่มีอยู่ (ไม่ว่าจะโดยการสืบทอดหรือการแทนที่) แต่เป็นการยากที่จะเพิ่มการดำเนินการใหม่เนื่องจากโดยพื้นฐานแล้วหมายถึงการแก้ไข คลาส / วัตถุที่มีอยู่

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

ที่นี่การเพิ่มประเภทโหนดใหม่เป็นเรื่องง่ายเพราะคุณจะสืบทอดแทนที่หรือใช้การดำเนินการที่จำเป็นทั้งหมด แต่การเพิ่มการดำเนินการใหม่นั้นทำได้ยากเนื่องจากคุณต้องเพิ่มลงในคลาสลีฟทั้งหมดหรือในคลาสฐานดังนั้นการแก้ไขที่มีอยู่ รหัส.

หลายภาษามีโครงสร้างหลายอย่างสำหรับการแก้ปัญหานิพจน์: Haskell มีประเภทของคลาส, Scala มีอาร์กิวเมนต์โดยนัย, แร็กเก็ตมีหน่วย, Go มีอินเทอร์เฟซ, CLOS และ Clojure มีหลายวิธี นอกจากนี้ยังมี "โซลูชัน" ที่พยายามแก้ไข แต่ล้มเหลวไม่ทางใดก็ทางหนึ่ง: อินเทอร์เฟซและวิธีการขยายใน C # และ Java, Monkeypatching ใน Ruby, Python, ECMAScript

สังเกตว่าจริงๆแล้ว Clojure มีกลไกในการแก้ปัญหานิพจน์: Multimethods อยู่แล้ว ปัญหาที่ OO มีกับ EP คือพวกเขารวมการดำเนินการและประเภทเข้าด้วยกัน ด้วย Multimethods จะแยกกัน ปัญหาที่ FP มีคือพวกเขารวมการดำเนินการและการเลือกปฏิบัติกรณีเข้าด้วยกัน อีกครั้งด้วย Multimethods จะแยกกัน

ลองเปรียบเทียบ Protocols กับ Multimethods เนื่องจากทั้งสองทำสิ่งเดียวกัน หรือจะพูดอีกอย่างว่าทำไมต้องเป็นโปรโตคอลถ้าเรามี Multimethods อยู่แล้ว?

สิ่งสำคัญที่ Protocols นำเสนอเหนือ Multimethods คือ Grouping: คุณสามารถจัดกลุ่มฟังก์ชันต่างๆเข้าด้วยกันและพูดว่า "3 ฟังก์ชันนี้รวมกันเป็น Protocol Foo" คุณไม่สามารถทำได้ด้วย Multimethods พวกมันมักจะยืนหยัดด้วยตัวเอง ตัวอย่างเช่นคุณสามารถประกาศว่าStackพิธีสารประกอบด้วยทั้งpushและpopฟังก์ชั่นด้วยกัน

แล้วทำไมไม่เพิ่มความสามารถในการจัดกลุ่ม Multimethods เข้าด้วยกัน? มีเหตุผลเชิงปฏิบัติอย่างแท้จริงและเป็นสาเหตุที่ฉันใช้คำว่า "ประสิทธิภาพ" ในประโยคเกริ่นนำนั่นคือประสิทธิภาพ

Clojure เป็นภาษาโฮสต์ กล่าวคือได้รับการออกแบบมาโดยเฉพาะให้ทำงานบนแพลตฟอร์มของภาษาอื่น และปรากฎว่าแทบทุกแพลตฟอร์มที่คุณต้องการให้ Clojure ทำงานบน (JVM, CLI, ECMAScript, Objective-C) มีการสนับสนุนประสิทธิภาพสูงโดยเฉพาะสำหรับการจัดส่งเฉพาะประเภทของอาร์กิวเมนต์แรกเท่านั้น Clojure Multimethods OTOH ส่งในคุณสมบัติโดยพลการของขัดแย้งทั้งหมด

ดังนั้นโปรโตคอล จำกัด คุณจะจัดส่งเฉพาะในครั้งแรกอาร์กิวเมนต์และเฉพาะกับชนิดของมัน (หรือเป็นกรณีพิเศษในnil)

นี่ไม่ใช่ข้อ จำกัด สำหรับแนวคิดของ Protocols per se แต่เป็นทางเลือกในทางปฏิบัติในการเข้าถึงการเพิ่มประสิทธิภาพการทำงานของแพลตฟอร์มพื้นฐาน โดยเฉพาะอย่างยิ่งหมายความว่าโปรโตคอลมีการแมปที่ไม่สำคัญกับอินเตอร์เฟส JVM / CLI ซึ่งทำให้รวดเร็วมาก เร็วพอที่จะสามารถเขียนส่วนเหล่านั้นของ Clojure ซึ่งปัจจุบันเขียนด้วย Java หรือ C # ใน Clojure เองได้

Clojure มี Protocols อยู่แล้วตั้งแต่เวอร์ชัน 1.0: Seqเป็น Protocol เป็นต้น แต่จนถึง 1.2 คุณไม่สามารถเขียนโปรโตคอลใน Clojure ได้คุณต้องเขียนด้วยภาษาโฮสต์


ขอบคุณสำหรับคำตอบอย่างละเอียด แต่คุณสามารถชี้แจงประเด็นของคุณเกี่ยวกับ Ruby ได้ ฉันคิดว่าความสามารถในการ (อีกครั้ง) กำหนดวิธีการของคลาสใด ๆ (เช่น String, Fixnum) ใน Ruby นั้นคล้ายคลึงกับ defprotocol ของ Clojure
defhlt

3
บทความที่ดีเยี่ยมเกี่ยวกับปัญหาการแสดงออกและโปรโตคอลของ clojure - ibm.com/developerworks/library/j-clojure-protocols
navgeet

ขออภัยที่ต้องโพสต์ความคิดเห็นเกี่ยวกับคำตอบเก่า ๆ แต่คุณช่วยอธิบายให้ละเอียดได้ไหมว่าเหตุใดส่วนขยายและอินเทอร์เฟซ (C # / Java) จึงไม่ใช่วิธีแก้ปัญหา Expression ที่ดี
Onorio Catenacci

Java ไม่มีส่วนขยายในแง่ที่ใช้คำนี้
user100464

Ruby มีการปรับแต่งซึ่งทำให้การปะลิงล้าสมัย
Marcin Bilski

65

ฉันพบว่าการคิดว่าโปรโตคอลมีแนวคิดคล้ายกับ "อินเทอร์เฟซ" ในภาษาเชิงวัตถุเช่น Java เป็นประโยชน์มากที่สุด โปรโตคอลกำหนดชุดฟังก์ชันนามธรรมที่สามารถนำไปใช้อย่างเป็นรูปธรรมสำหรับวัตถุที่กำหนด

ตัวอย่าง:

(defprotocol my-protocol 
  (foo [x]))

กำหนดโปรโตคอลด้วยฟังก์ชันหนึ่งที่เรียกว่า "foo" ซึ่งทำหน้าที่กับพารามิเตอร์ "x" หนึ่งตัว

จากนั้นคุณสามารถสร้างโครงสร้างข้อมูลที่ใช้โปรโตคอลเช่น

(defrecord constant-foo [value]  
  my-protocol
    (foo [x] value))

(def a (constant-foo. 7))

(foo a)
=> 7

โปรดทราบว่าที่นี่อ็อบเจ็กต์ที่ใช้โปรโตคอลจะถูกส่งผ่านเป็นพารามิเตอร์แรกxซึ่งค่อนข้างเหมือนกับพารามิเตอร์ "this" โดยนัยในภาษาเชิงวัตถุ

หนึ่งในคุณสมบัติที่มีประสิทธิภาพมากและมีประโยชน์ของโปรโตคอลคือการที่คุณสามารถขยายพวกเขาไปยังวัตถุแม้ว่าวัตถุที่ไม่ได้ถูกออกแบบมาเพื่อรองรับโปรโตคอล เช่นคุณสามารถขยายโปรโตคอลด้านบนเป็นคลาส java.lang.String ได้หากคุณต้องการ:

(extend-protocol my-protocol
  java.lang.String
    (foo [x] (.length x)))

(foo "Hello")
=> 5

2
> เช่นเดียวกับพารามิเตอร์ "this" โดยนัยในภาษาเชิงวัตถุฉันสังเกตเห็นว่า var ที่ส่งผ่านไปยังฟังก์ชันโปรโตคอลมักถูกเรียกthisในรหัส Clojure ด้วย
Kris
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.