ฉันกำลังพยายามทำความเข้าใจโปรโตคอลของ clojure และปัญหาที่ควรแก้ไข ใครมีคำอธิบายที่ชัดเจนเกี่ยวกับโปรโตคอล clojure คืออะไร?
ฉันกำลังพยายามทำความเข้าใจโปรโตคอลของ clojure และปัญหาที่ควรแก้ไข ใครมีคำอธิบายที่ชัดเจนเกี่ยวกับโปรโตคอล clojure คืออะไร?
คำตอบ:
วัตถุประสงค์ของโปรโตคอลใน 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 ได้คุณต้องเขียนด้วยภาษาโฮสต์
ฉันพบว่าการคิดว่าโปรโตคอลมีแนวคิดคล้ายกับ "อินเทอร์เฟซ" ในภาษาเชิงวัตถุเช่น 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
this
ในรหัส Clojure ด้วย