Giải thích đơn giản về giao thức clojure


131

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?


7
Giao thức Clojure 1.2 trong 27 phút: vimeo.com/11236603
miku

3
Sự tương đồng rất gần với Giao thức là Đặc điểm (mixins) trong Scala: stackoverflow.com/questions/4508125/ (
Vasil Remeniuk

Câu trả lời:


284

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ó. 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,, casekhớ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ự đã 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 đã Đ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 StackGiao thức bao gồm cả a pushpophà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: Seqlà 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ủ.


Cảm ơn bạn đã trả lời thấu đáo như vậy, nhưng bạn có thể làm rõ quan điểm của mình về Ruby. Tôi cho rằng khả năng (tái) định nghĩa các phương thức của bất kỳ lớp nào (ví dụ String, Fixnum) trong Ruby tương tự như defprotatio của Clojure.
defhlt

3
Một bài viết tuyệt vời về Vấn đề Biểu hiện và các giao thức của clojure - ibm.com/developerworks/l
Library / j

Xin lỗi để đăng nhận xét về một câu trả lời cũ như vậy nhưng bạn có thể giải thích lý do tại sao các tiện ích mở rộng và giao diện (C # / Java) không phải là một giải pháp tốt cho Vấn đề Biểu hiện?
Onorio Catenacci

Java không có phần mở rộng theo nghĩa là thuật ngữ được sử dụng ở đây.
100464

Ruby có những tinh chỉnh khiến khỉ vá lỗi thời.
Marcin Bilski

64

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

1
> giống như tham số "this" ẩn trong ngôn ngữ hướng đối tượng Tôi nhận thấy var được truyền cho các hàm giao thức cũng thường được gọi thistrong mã Clojure.
Kris
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.