Clojure protokollerinin basit açıklaması


Yanıtlar:


284

Clojure'deki Protokoller'in amacı, İfade Problemini verimli bir şekilde çözmektir.

Öyleyse, İfade Problemi nedir? Temel genişletilebilirlik sorununu ifade eder: Programlarımız, işlemleri kullanarak veri türlerini işler. Programlarımız geliştikçe, onları yeni veri türleri ve yeni işlemlerle genişletmemiz gerekiyor. Ve özellikle, mevcut veri türleri ile çalışan yeni işlemler ekleyebilmek istiyoruz ve mevcut işlemlerle çalışan yeni veri türleri eklemek istiyoruz. Bunun gerçek bir uzantı olmasını istiyoruz , yani mevcut olanı değiştirmek istemiyoruz.program, mevcut soyutlamalara saygı duymak istiyoruz, uzantılarımızın ayrı modüller olmasını, ayrı ad alanlarında, ayrı derlenmesini, ayrı konuşlandırılmasını, ayrı tip kontrol edilmesini istiyoruz. Tür açısından güvenli olmalarını istiyoruz. [Not: Bunların hepsi tüm dillerde anlamlı değildir. Ancak, örneğin, onları güvenli hale getirme hedefi Clojure gibi bir dilde bile anlamlıdır. Tip güvenliğini statik olarak kontrol edemiyor olmamız, kodumuzun rastgele kırılmasını istediğimiz anlamına gelmez, değil mi?]

İfade Problemi, bir dilde bu kadar genişleyebilirliği nasıl sağlarsınız?

Prosedürel ve / veya fonksiyonel programlamanın tipik naif uygulamaları için, yeni operasyonlar (prosedürler, fonksiyonlar) eklemenin çok kolay olduğu, ancak temelde operasyonların bazılarını kullanan veri türleri ile çalıştığı için yeni veri türleri eklemenin çok zor olduğu ortaya çıktı. bir çeşit vaka ayrımı ( switch,, caseörüntü eşleştirme) ve bunlara yeni vakalar eklemeniz gerekir, yani mevcut kodu değiştirin:

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)

Şimdi, yeni bir işlem eklemek istiyorsanız, örneğin yazım denetimi, bu kolaydır, ancak yeni bir düğüm türü eklemek istiyorsanız, tüm işlemlerde mevcut tüm kalıp eşleştirme ifadelerini değiştirmeniz gerekir.

Ve tipik saf OO için, tam tersi bir probleminiz var: mevcut işlemlerle çalışan yeni veri türlerini eklemek kolaydır (bunları devralarak veya geçersiz kılarak), ancak temelde değişiklik yapmak anlamına geldiğinden yeni işlemler eklemek zordur. mevcut sınıflar / nesneler.

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

Burada, yeni bir düğüm türü eklemek kolaydır, çünkü gerekli tüm işlemleri miras alırsınız, geçersiz kılarsınız veya uygularsınız, ancak yeni bir işlem eklemek zordur, çünkü bunu tüm yaprak sınıflara veya bir temel sınıfa eklemeniz gerekir, böylece mevcut kodu.

Çeşitli dillerin İfade Problemini çözmek için çeşitli yapıları vardır: Haskell'in tip sınıfları vardır, Scala'nın üstü kapalı argümanları vardır, Racket'in Birimleri vardır, Go'nun Arayüzleri vardır, CLOS ve Clojure'un Çoklu Yöntemleri vardır. Bunu çözmeye çalışan , ancak bir şekilde başarısız olan "çözümler" de vardır : C # ve Java'da Arayüzler ve Genişletme Yöntemleri, Ruby'de Monkeypatching, Python, ECMAScript.

Clojure'un aslında İfade Problemini çözmek için bir mekanizmaya sahip olduğuna dikkat edin: Çoklu Yöntemler. OO'nun EP ile yaşadığı sorun, operasyonları ve türleri bir araya getirmeleridir. Multimethods ile bunlar ayrıdır. FP'nin sahip olduğu sorun, operasyon ve vaka ayrımcılığını bir araya getirmeleridir. Yine Multimethods ile ayrıdırlar.

Öyleyse, Protokolleri Multimethods ile karşılaştıralım, çünkü ikisi de aynı şeyi yapıyor. Veya, Diğer bir deyişle için: Neden Protokolleri biz zaten eğer var Multimethods?

Protokollerin Multimethods üzerinden sunduğu en önemli şey Gruplandırmadır: Birden fazla işlevi birlikte gruplayabilir ve "bu 3 işlevi birlikte Protokol oluşturur Foo" diyebilirsiniz . Bunu Multimethods ile yapamazsınız, onlar her zaman kendi başlarına kalırlar. Örneğin, bir beyan olabilir StackProtokol oluşur hem a pushve popfonksiyon birlikte ile .

Öyleyse neden Multimethods'u bir araya getirme özelliğini eklemiyorsunuz? Tamamen pragmatik bir neden var ve bu yüzden giriş cümlemde "verimli" kelimesini kullandım: performans.

Clojure, barındırılan bir dildir. Yani, başka bir dil platformunun üzerinde çalışacak şekilde özel olarak tasarlanmıştır . Ve Clojure'un üzerinde çalışmasını istediğiniz hemen hemen her platformun (JVM, CLI, ECMAScript, Objective-C) yalnızca ilk argümanın türüne göre gönderim için özel yüksek performanslı desteğe sahip olduğu ortaya çıktı. Clojure Multimethods OTOH , tüm argümanların keyfi özelliklerini gönderir .

Yani, Protokoller sen gönderme kısıtlamak sadece üzerinde ilk argüman ve sadece (veya özel bir durum olarak türüne nil).

Bu, Protokoller fikrinin kendi başına bir sınırlaması değildir, temeldeki platformun performans optimizasyonlarına erişmek için pragmatik bir seçimdir. Özellikle, Protokollerin JVM / CLI Arayüzlerine önemsiz bir eşlemesi olduğu anlamına gelir, bu da onları çok hızlı yapar. Aslında, Clojure'un şu anda Java veya C # ile yazılmış olan Clojure bölümlerini yeniden yazabilmek için yeterince hızlı.

Clojure, aslında 1.0 sürümünden beri Protokollere sahiptir: Seqörneğin bir Protokoldür. Fakat 1.2'ye kadar Clojure'da Protokoller yazamazdınız, bunları ana dilde yazmanız gerekiyordu.


Böylesine kapsamlı bir cevap için teşekkür ederim ama Ruby ile ilgili düşüncenizi açıklayabilir misiniz? Ruby'de herhangi bir sınıfın (örn. String, Fixnum) yöntemlerini (yeniden) tanımlama yeteneğinin Clojure'un defprotokoluna benzediğini düşünüyorum.
defhlt

3
İfade Sorunu ve clojure protokolleri hakkında mükemmel bir makale - ibm.com/developerworks/library/j-clojure-protocols
navgeet

Bu kadar eski bir cevaba yorum yaptığım için üzgünüz, ancak uzantıların ve arayüzlerin (C # / Java) İfade Problemi için neden iyi bir çözüm olmadığını açıklayabilir misiniz?
Onorio Catenacci

Java, terimin burada kullanıldığı anlamda uzantılara sahip değildir.
user100464

Ruby, maymun yamalarını geçersiz kılan iyileştirmelere sahiptir.
Marcin Bilski

65

Protokollerin kavramsal olarak Java gibi nesne yönelimli dillerdeki bir "arayüz" e benzediğini düşünmenin en yararlı olduğunu düşünüyorum. Bir protokol, belirli bir nesne için somut bir şekilde uygulanabilen soyut bir işlevler kümesini tanımlar.

Bir örnek:

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

Bir "x" parametresine etki eden "foo" adlı bir işleve sahip bir protokol tanımlar.

Daha sonra protokolü uygulayan veri yapıları oluşturabilirsiniz, örn.

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

(def a (constant-foo. 7))

(foo a)
=> 7

Burada protokolü uygulayan nesnenin birinci parametre olarak aktarıldığına dikkat edin x- nesne yönelimli dillerdeki örtük "this" parametresi gibi.

Protokollerin çok güçlü ve kullanışlı özelliklerinden biri , nesne başlangıçta protokolü desteklemek için tasarlanmamış olsa bile bunları nesnelere genişletebilmenizdir . Örneğin, isterseniz yukarıdaki protokolü java.lang.String sınıfına genişletebilirsiniz:

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

(foo "Hello")
=> 5

2
> nesne yönelimli dilde örtük "this" parametresi gibi, protokol işlevlerine iletilen değişkenin genellikle thisClojure kodunda da çağrıldığını fark ettim .
Kris
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.