Tôi đang cố gắng để hiểu các giao thức clojure và vấn đề họ phải giải quyết. Có ai có một lời giải thích rõ ràng về những gì và những gì của giao thức clojure?
Tôi đang cố gắng để hiểu các giao thức clojure và vấn đề họ phải giải quyết. Có ai có một lời giải thích rõ ràng về những gì và những gì của giao thức clojure?
Câu trả lời:
Mục đích của Giao thức trong Clojure là giải quyết vấn đề Biểu hiện một cách hiệu quả.
Vì vậy, vấn đề biểu hiện là gì? Nó đề cập đến vấn đề cơ bản về khả năng mở rộng: các chương trình của chúng tôi thao tác các loại dữ liệu bằng các thao tác. Khi các chương trình của chúng tôi phát triển, chúng tôi cần mở rộng chúng với các loại dữ liệu mới và các hoạt động mới. Và đặc biệt, chúng tôi muốn có thể thêm các hoạt động mới hoạt động với các loại dữ liệu hiện có và chúng tôi muốn thêm các loại dữ liệu mới hoạt động với các hoạt động hiện có. Và chúng tôi muốn đây là phần mở rộng thực sự , tức là chúng tôi không muốn sửa đổi phần hiện cóchương trình, chúng tôi muốn tôn trọng các khái niệm trừu tượng hiện có, chúng tôi muốn các tiện ích mở rộng của chúng tôi là các mô-đun riêng biệt, trong các không gian tên riêng biệt, được biên dịch riêng, triển khai riêng, kiểm tra loại riêng. Chúng tôi muốn chúng là loại an toàn. [Lưu ý: không phải tất cả những điều này có ý nghĩa trong tất cả các ngôn ngữ. Nhưng, ví dụ, mục tiêu để có chúng an toàn kiểu có ý nghĩa ngay cả trong một ngôn ngữ như Clojure. Chỉ vì chúng tôi không thể kiểm tra tĩnh loại an toàn không có nghĩa là chúng tôi muốn mã của mình bị phá vỡ ngẫu nhiên, phải không?]
Vấn đề biểu hiện là, làm thế nào để bạn thực sự cung cấp khả năng mở rộng như vậy trong một ngôn ngữ?
Hóa ra, đối với việc triển khai chương trình thủ tục và / hoặc chức năng ngây thơ điển hình, rất dễ dàng để thêm các hoạt động mới (thủ tục, chức năng), nhưng rất khó để thêm các loại dữ liệu mới, vì về cơ bản các hoạt động này hoạt động với các loại dữ liệu sử dụng một số loại loại phân biệt trường hợp ( switch
,, case
khớp mẫu) và bạn cần thêm trường hợp mới vào chúng, tức là sửa đổi mã hiện có:
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)
Bây giờ, nếu bạn muốn thêm một thao tác mới, giả sử, kiểm tra kiểu, thật dễ dàng, nhưng nếu bạn muốn thêm một loại nút mới, bạn phải sửa đổi tất cả các biểu thức khớp mẫu hiện có trong tất cả các hoạt động.
Và đối với OO ngây thơ điển hình, bạn có một vấn đề hoàn toàn ngược lại: thật dễ dàng để thêm các loại dữ liệu mới hoạt động với các hoạt động hiện có (bằng cách kế thừa hoặc ghi đè chúng), nhưng khó có thể thêm các hoạt động mới, vì về cơ bản điều đó có nghĩa là sửa đổi các lớp / đối tượng hiện có.
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
Ở đây, việc thêm một loại nút mới rất dễ dàng, bởi vì bạn kế thừa, ghi đè hoặc thực hiện tất cả các hoạt động cần thiết, nhưng thêm một hoạt động mới là khó, bởi vì bạn cần thêm nó vào tất cả các lớp lá hoặc vào một lớp cơ sở, do đó sửa đổi hiện có mã.
Một số ngôn ngữ có một số cấu trúc để giải quyết vấn đề Biểu hiện: Haskell có kiểu chữ, Scala có các đối số ngầm, Vợt có Đơn vị, Go có Giao diện, CLOS và Clojure có Đa phương thức. Ngoài ra còn có các "giải pháp" cố gắng giải quyết nó, nhưng thất bại theo cách này hay cách khác: Giao diện và Phương thức mở rộng trong C # và Java, Monkeypatching trong Ruby, Python, ECMAScript.
Lưu ý rằng Clojure thực sự đã có một cơ chế để giải quyết vấn đề Biểu hiện: Đa phương thức. Vấn đề mà OO gặp phải với EP là họ kết hợp các hoạt động và loại với nhau. Với Multimethods chúng là riêng biệt. Vấn đề mà FP gặp phải là họ bó hoạt động và phân biệt trường hợp với nhau. Một lần nữa, với Multimethods chúng là riêng biệt.
Vì vậy, hãy so sánh các giao thức với Multimethod, vì cả hai đều làm điều tương tự. Hoặc, nói một cách khác: Tại sao các Giao thức nếu chúng ta đã có Đa phương thức?
Điều chính mà Giao thức cung cấp qua Đa phương thức là Nhóm: bạn có thể nhóm nhiều chức năng lại với nhau và nói "3 chức năng này cùng nhau tạo thành Giao thức Foo
". Bạn không thể làm điều đó với Multimethod, chúng luôn tự đứng. Ví dụ: bạn có thể tuyên bố rằng Stack
Giao thức bao gồm cả a push
và pop
hàm cùng nhau .
Vì vậy, tại sao không thêm khả năng để nhóm Multimethods lại với nhau? Có một lý do hoàn toàn thực dụng và đó là lý do tại sao tôi sử dụng từ "hiệu quả" trong câu giới thiệu của mình: hiệu suất.
Clojure là một ngôn ngữ được lưu trữ. Tức là nó được thiết kế đặc biệt để chạy trên nền tảng của ngôn ngữ khác . Và nó chỉ ra rằng hầu như bất kỳ nền tảng nào mà bạn muốn Clojure chạy trên (JVM, CLI, ECMAScript, Objective-C) đều có hỗ trợ hiệu suất cao chuyên biệt để chỉ gửi loại đối số đầu tiên. Clojure Multimethods OTOH gửi về các thuộc tính tùy ý của tất cả các đối số .
Vì vậy, giao thức hạn chế bạn với cử chỉ trên đầu tranh cãi và chỉ vào loại của nó (hoặc là một trường hợp đặc biệt trên nil
).
Đây không phải là một hạn chế về ý tưởng của Giao thức mỗi lần, nó là một lựa chọn thực tế để có quyền truy cập vào tối ưu hóa hiệu suất của nền tảng cơ bản. Cụ thể, điều đó có nghĩa là các Giao thức có ánh xạ tầm thường đến Giao diện JVM / CLI, khiến chúng rất nhanh. Trên thực tế, đủ nhanh để có thể viết lại những phần đó của Clojure hiện đang được viết bằng Java hoặc C # bằng chính Clojure.
Clojure thực sự đã có Giao thức kể từ phiên bản 1.0: Seq
là một Giao thức chẳng hạn. Nhưng cho đến ngày 1.2, bạn không thể viết Giao thức bằng Clojure, bạn phải viết chúng bằng ngôn ngữ máy chủ.
Tôi thấy hữu ích nhất khi nghĩ về các giao thức giống như khái niệm với một "giao diện" trong các ngôn ngữ hướng đối tượng như Java. Một giao thức định nghĩa một tập hợp các hàm trừu tượng có thể được thực hiện theo cách cụ thể cho một đối tượng nhất định.
Một ví dụ:
(defprotocol my-protocol
(foo [x]))
Xác định một giao thức với một hàm gọi là "foo" hoạt động trên một tham số "x".
Sau đó, bạn có thể tạo cấu trúc dữ liệu thực hiện giao thức, ví dụ:
(defrecord constant-foo [value]
my-protocol
(foo [x] value))
(def a (constant-foo. 7))
(foo a)
=> 7
Lưu ý rằng ở đây, đối tượng thực hiện giao thức được truyền dưới dạng tham số đầu tiên x
- hơi giống tham số "this" ẩn trong các ngôn ngữ hướng đối tượng.
Một trong những tính năng rất mạnh mẽ và hữu ích của các giao thức là bạn có thể mở rộng chúng cho các đối tượng ngay cả khi đối tượng ban đầu không được thiết kế để hỗ trợ giao thức . ví dụ: bạn có thể mở rộng giao thức ở trên sang lớp java.lang.String nếu bạn muốn:
(extend-protocol my-protocol
java.lang.String
(foo [x] (.length x)))
(foo "Hello")
=> 5
this
trong mã Clojure.