Clojure protokollerini ve hangi sorunu çözmeleri gerektiğini anlamaya çalışıyorum. Clojure protokollerinin ne ve neden olduğuna dair net bir açıklaması olan var mı?
Clojure protokollerini ve hangi sorunu çözmeleri gerektiğini anlamaya çalışıyorum. Clojure protokollerinin ne ve neden olduğuna dair net bir açıklaması olan var mı?
Yanıtlar:
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 Stack
Protokol oluşur hem a push
ve pop
fonksiyon 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.
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
this
Clojure kodunda da çağrıldığını fark ettim .