클로저 프로토콜과 그들이 해결해야 할 문제를 이해하려고합니다. 클로저 프로토콜의 내용과 이유에 대한 명확한 설명이 있습니까?
답변
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에는 형식 클래스가 있고 스칼라는 암시 적 인수가 있으며 Racket에는 단위가 있으며 Go에는 인터페이스가 있으며 CLOS 및 Clojure에는 다중 메서드가 있습니다. C # 및 Java의 인터페이스 및 확장 메소드, Ruby의 Monkeypatching, Python, ECMAScript 등의 방법으로 해결 하려고 시도 하는 “솔루션”도 있습니다 .
Clojure에는 실제로 Expression Problem : Multimethods를 해결하는 메커니즘 이 이미 있습니다 . 오픈 오피스가 EP와 함께 갖는 문제는 그들이 오퍼레이션과 타입을 함께 묶는다는 것입니다. 멀티 메소드는 서로 분리되어 있습니다. FP의 문제점은 작업과 사례 차별을 함께 묶는 것입니다. 다시 말하지만 멀티 메소드에서는 분리되어 있습니다.
프로토콜과 멀티 메소드를 비교해 봅시다. 둘 다 같은 일을하기 때문입니다. 또는 다른 말로 표현하자면, 이미 멀티 메소드 가있는 경우 왜 프로토콜 입니까?
프로토콜이 멀티 메소드에 제공하는 주요 기능은 그룹화입니다. 여러 기능을 함께 그룹화하고 “이 3 가지 기능을 함께 프로토콜 Foo
” 이라고 말할 수 있습니다 . 멀티 메소드로는 그렇게 할 수 없으며 항상 그들 자신 만의 것입니다. 예를 들어, 당신은 선언 할 수 Stack
프로토콜로 구성되어 모두push
과 pop
기능을 함께 .
그렇다면 왜 멀티 메소드를 그룹화하는 기능을 추가하지 않겠습니까? 순전히 실용적인 이유가 있으며, 이것이 제가 소개 문장에서“효율적”이라는 단어를 사용하는 이유입니다 : 성능.
Clojure는 호스팅 언어입니다. 즉, 다른 언어 플랫폼 위에서 실행되도록 특별히 설계되었습니다 . Clojure를 실행하려는 거의 모든 플랫폼 (JVM, CLI, ECMAScript, Objective-C)은 첫 번째 인수 유형 에만 디스패치하기위한 특수한 고성능 지원을 가지고 있음이 밝혀졌습니다 . Clojure Multimethods OTOH 는 모든 인수의 임의의 속성 을 디스패치합니다 .
그래서, 프로토콜은 파견 제한 에만 온 첫번째 인수 및 단지 (또는에 특별한 경우로 그 유형에 nil
).
이것은 프로토콜 자체의 개념에 대한 제한이 아니며, 기본 플랫폼의 성능 최적화에 액세스하기위한 실용적인 선택입니다. 특히, 이는 프로토콜이 JVM / CLI 인터페이스에 대한 사소한 매핑을 갖기 때문에 매우 빠릅니다. 사실 현재 Clojure 자체에서 Java 또는 C #으로 작성된 Clojure의 해당 부분을 다시 작성할 수있을 정도로 빠릅니다.
Clojure는 실제로 1.0 버전 이후 이미 프로토콜을 가지고 있습니다 : Seq
예를 들어 프로토콜입니다. 그러나 1.2까지 Clojure에서 프로토콜을 작성할 수 없었으므로 호스트 언어로 프로토콜을 작성해야했습니다.
답변
프로토콜을 Java와 같은 객체 지향 언어의 “인터페이스”와 개념적으로 비슷하다고 생각하면 가장 도움이됩니다. 프로토콜은 주어진 객체에 대해 구체적으로 구현 될 수있는 추상 함수 집합을 정의합니다.
예를 들면 :
(defprotocol my-protocol
(foo [x]))
하나의 매개 변수 “x”에서 작동하는 “foo”라는 하나의 함수로 프로토콜을 정의합니다.
그런 다음 프로토콜을 구현하는 데이터 구조를 만들 수 있습니다.
(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