Scala üzerine ikinci yazımızda bu JSM dilinin sağladığı sınıf tanımlama imkânına daha yakından bakacağız ve bunu yaparken de Java ile olan farklılıklara değinmeye çalışacağız.1 Matematikteki kesirli sayı kavramını soyutlayan aşağıdaki sınıf iskeletiyle başlayalım.2
Java'dan gelenleri başlarda şaşırtabilecek bir diğer nokta, nesnenin yaratılması esnasında yapıcıya geçirilmesi beklenen argümanların özelliklerini belirten parametre listesinin sınıf adı sonrasında bulunmasıdır. Buna göre,
Haklı olarak,
Bir diğer farklılık, Java'da aynı adlı metotların ezilmesi ile öykünülen varsayılan argümanların kullanımı. Scala 2.8'den başlayarak geçerli olan bu özellik sayesinde, varsayılan değerin uygun olması durumunda, argümanın es geçilmesi de olanaklıdır. Örneğin, yukarıdaki sınıf tanımının geçerli olduğu bir ortamda,
Scala 2.8 ile birlikte, ileti gönderileri ve metot çağrıları sırasında argümanların parametre adları kullanılarak konumlarından farklı bir sırada geçirilmesi de olanaklı kılınmıştır. Buna göre, önceki paragraflardaki
Yukarıdaki örnekte varsayılan argümanlar yardımıyla sağlanan değişik sayıda argümanlarla kullanılabilen yapıcı metotlar görüntüsü, yardımcı yapıcı metotlar yoluyla da sağlanabilir. Varsayılan argümanlarla birlikte de yararlanılabilecek bu yaklaşımda, programcı ilk iş olarak uygun argümanlarla birincil yapıcıyı veya diğer yardımcı yapıcılardan birini çağıran
Java'dan tanıdık gelecek bir nokta, üstsınıflardan kalıtlanan bir metodun ezilmekte olduğunu bildiren
Soysal olan
Yukarıdaki kod parçasının ortaya koyduğu bir diğer önemli nokta, Scala'nın, Java'nın aksine, işleçlerin aşırı yüklenmesini—ya da, işin doğrusunu söylemek gerekirse, bu tür bir yanılsamayı yaratabilecek özellikleri—desteklediğidir. Öncelikle, tanımlayıcı adlarının oluşturulmasında kullanılan karakterler kullanageldiğimiz işleçlerin simgelerini de kapsayan daha geniş bir yelpazeden seçilebilir.4 Ayrıca, tek argüman alan iletiler işleç ortada sözdizimiyle de kullanılabilir. Örneğin
Kod parçamızda dikkat çeken bir diğer bölüm, tek-örneklimizin tanımında geçen
Tanımlanmış bir çeşni, bir diğer çeşni tarafından kalıtlanmak suretiyle geliştirilebileceği gibi, içerdiği soyut öğelerin sağlanması ve/veya bazı öğelerinin ezilmesi yoluyla bir sınıfa katılabilir. Her iki durum da
Bir sınıfa çeşni katılması nesnenin yaratıldığı noktada, dinamik olarak da gerçekleştirilebilir. Örnek olarak,
Java'daki bileşke tür ve ilkel tür ayrımı Scala'da bire bir karşılık bulmaz; ilkel türlerin ele alınışını C# diline benzeterek anlamak daha kolay olacaktır. Çünkü, arka planda işlemcinin desteklediği türlerden birine eşleştirilerek işlenen bu çeşit değerler, kaynak kod düzeyinde kalıtlanarak geliştirilemeyen—yani
Bu haliyle çeşnilerin soyut sınıflardan çok da farklı olmadığını düşünebilirsiniz: tıpkı soyut sınıflarda olduğu gibi, çeşniler altalan tanımlarının yanısıra iletiler ve bu iletilerin bazıları veya tümü için gerçekleştirimler içerebilir. Buna karşılık, çeşniler yardımcı yapıcılara sahip olamaz; ek olarak, birincil yapıcılar argüman alamadığı gibi kalıtladıkları türlerin yapıcılarına argüman geçiremez. Ayrıca, (soyut) sınıflar için tekli kalıtlama geçerliyken çeşniler birden çok çeşniden kalıtlayabilir. Bundan dolayı bazılarınız, önceki paragraflardaki kategori sözcüğünün kullanımının da katkısıyla, çeşnilerin biraz da arayüzlere benzediğini düşünebilir. İki grubun da bir yere kadar haklı olduğu söylenebilir. Kimin haklı olduğunu ilan etmeden önce, Scala derleyicisinin çoklu sınıf kalıtlamanın desteklenmediği JSM üzerinde nasıl olur da soyut sınıf özellikleri sergileyen çeşnilerin çoklu kalıtımını olduruyor, ona bir bakalım. Bunun için sınıf dosyalarını tanımlanan türün üyelerini listeleyerek tersine bütünleştiren
Dikkatinizi çekeceğimiz son nokta, çeşni katılmış bir sınıfın nesnesine gönderilen iletilerin işlemesi sırasında
Kesir.scala
package com.ta.matematik ... class Kesir(pay: Long = 1, payda: Long = 1) { require(payda != 0) private var (_pay, _payda) = (pay, payda) sadeleştir() ... private def sadeleştir() = { ... } } // Kesir sınıfının sonuJava'dan gelenlerin gözüne çarpacak farklılıklardan belki de ilki,
public
niteleyicisinin sırra kadem basmış olması. Bu durumun sebebi, paket çapında erişimin varsayıldığı Java'dan farklı olarak, Scala'da programlama öğelerinin nitelenmemeleri halinde public
erişime sahip kılınmasıdır. Dolayısıyla, Kesir
ve erişim niteleyicisi bulunmayan tüm sınıf öğeleri herkes tarafından kullanılabilecektir. Bunun uygun olmaması halinde, programcının niyetini protected
veya private
niteleyicilerinden birini kullanarak bildirmesi gerekir. Bu noktada, protected
niteleyicisinin paket içindeki türleri kapsamayıp, söz konusu öğeyi sadece o anki türden doğrudan veya dolaylı bir biçimde kalıtlayan türlere erişilebilir kıldığını söylemekte yarar vardır.3 Ayrıca, paket çapında erişimden de bahsedilmediği dikkatinizi çekmiştir. Ancak, telaşa düşmeyin; nitelenmekte olan öğeyi içeren paket, sınıf veya tek-örnekli sınıf adlarından birinin protected
veya private
ile birlikte belirtilerek daha rafine erişim politikalarının ifade edilmesi mümkündür. Örneğin, private
yerine private[com.ta.matematik]
nitelemesinin yapılması, sadeleştir
adlı metodu sınıfa özel olmaktan çıkaracak ve com.ta.matematik
paketindeki tüm öğelere görünür kılacaktır.Java'dan gelenleri başlarda şaşırtabilecek bir diğer nokta, nesnenin yaratılması esnasında yapıcıya geçirilmesi beklenen argümanların özelliklerini belirten parametre listesinin sınıf adı sonrasında bulunmasıdır. Buna göre,
Kesir
sınıfının örneği ks1 = new Kesir(6, 10)
şeklinde yaratılabilecektir. İlkleme ise Kesir
sınıfının tanımındaki metotların dışında kalan tüm öğelerin koddaki geçiş sırasında işlenmesiyle yapılacaktır. Yani, sınıf gövdesindeki söz konusu öğeler nesne ilkleme bloğu görevini görecektir; Scala terminolojisiyle konuşacak olursak, sınıf gövdesindeki öğeler birincil yapıcı olarak ele alınacaktır. Örneğimiz üzerinden gidecek olursak; Predef
adlı tek örnekli sınıftan ithal edilen require
metodu ile geçirilen paydanın uygunluk denetiminin yapılmasını takiben, yaratılmakta olan nesnenin altalanları olan _pay
ve _payda
koşut bir biçimde pay
ve payda
ile ilklendikten sonra yaratılmakta olan kesir sadeleştirilecektir. Bu noktada, koşut atamanın tüm kollarının aynı anda işleniyormuş gibi düşünülmesi gerektiği unutulmamalıdır.Haklı olarak,
Predef
sınıfının nereden çıktığını sorabilirsiniz. Yanıtı, Scala derleyicisinin, Java derleyicisinin java.lang
paketini otomatikman ithal edilmiş kabul etmesindeki gibi, java.lang
ve scala
paketleriyle scala.Predef
tek örneklisini otomatikman tüm programlara ithal etmesinde yatar. Bunun sonucunda, Predef
tanımı da evrensel olarak görünür hale gelecek ve bazı işlemler öncesinde önkoşul denetimine yarayan require
metodu da yukarıdaki gibi kullanılabilecektir.Bir diğer farklılık, Java'da aynı adlı metotların ezilmesi ile öykünülen varsayılan argümanların kullanımı. Scala 2.8'den başlayarak geçerli olan bu özellik sayesinde, varsayılan değerin uygun olması durumunda, argümanın es geçilmesi de olanaklıdır. Örneğin, yukarıdaki sınıf tanımının geçerli olduğu bir ortamda,
tamsayı = new Kesir(7)
ve bir = new Kesir()
, sırasıyla, 7/1 ve 1/1 değerlerini temsil eden kesirli sayıları yaratacaktır. Bu noktada, varsayılan argümanların metot imzasının sonunda yer alması zorunluluğu unutulmamalıdır. Dolayısıyla, 1/7 kesrini temsil eden nesnenin yaratılması için her iki argümanın da geçirilmesi gerekecektir.Scala 2.8 ile birlikte, ileti gönderileri ve metot çağrıları sırasında argümanların parametre adları kullanılarak konumlarından farklı bir sırada geçirilmesi de olanaklı kılınmıştır. Buna göre, önceki paragraflardaki
ks
ve tamsayı
tanımlayıcılarının, sırasıyla, new Kesir(payda = 5, pay= 3)
ve new Kesir(pay= 7)
şeklinde tanımlanması mümkün olacaktır. Bu sayede, birBölüYedi = new Kesir(payda = 7)
tanımlamasının da geçerli olması sağlanarak 1/7 kesrine karşılık gelen nesnenin tek argüman geçirilerek yaratılması da mümkün olmaktadır.Yukarıdaki örnekte varsayılan argümanlar yardımıyla sağlanan değişik sayıda argümanlarla kullanılabilen yapıcı metotlar görüntüsü, yardımcı yapıcı metotlar yoluyla da sağlanabilir. Varsayılan argümanlarla birlikte de yararlanılabilecek bu yaklaşımda, programcı ilk iş olarak uygun argümanlarla birincil yapıcıyı veya diğer yardımcı yapıcılardan birini çağıran
this
adına sahip metotlar yazar. Örneğin, aşağıdaki kod parçasında iki argümanın da sağlanması durumunda birincil yapıcı çağrılırken, bir veya sıfır argümanın sağlanması halinde, sırasıyla, 7. ve 8. satırlardaki yardımcı yapıcılar çağrılacaktır. Dikkat ederseniz, her iki yapıcı da işini bir diğer yapıcıya havale ederek görmekte.... class Kesir(private var _pay: Long, private var _payda: Long) { require(_payda != 0) sadeleştir() ... def this(_pay: Long) = this(_pay, 1) def this() = this(1) def pay = _pay def payda = _payda ... override def toString() = _pay + "/" + _payda ... } // Kesir sınıfının sonuBirincil yapıcının parametre listesindeki değişiklik de gözünüze çarpmıştır, Tanımının önüne
var
niteleyicisinin konulmasıyla ilişkin parametrenin değişken içeriğe sahip kılınması nedeniyle, daha önceki kod parçasında olduğu gibi yapıcıya geçirilen argümanların değişken içerikli altalanları ilklemesi ve değişikliklerin bu altalanlarda yapılması artık gerekli değildir. Ancak, birincil yapıcıya özel bu durum, Scala'nın atıf geçirme (İng., pass by reference) yöntemini desteklediği şeklinde yorumlanmamalıdır; Java'da olduğu gibi, Scala'da da argümanlar ilişkin metoda değer geçirme (İng., pass by value) yöntemiyle sağlanır.Java'dan tanıdık gelecek bir nokta, üstsınıflardan kalıtlanan bir metodun ezilmekte olduğunu bildiren
override
niteleyicisidir. Ancak, Java dengi @Override
açımlamasının kullanımı seçimli olup sadece tavsiye edilirken, Scala'daki bu niteleyicinin kullanımı aynı imzaya sahip metotların varlığında zorunludur. Bu kurala uyulmaması, derleyicinin hata mesajıyla karşılanacaktır.toString
'i bir yerlerden gözünüzün ısırdığını düşünüyorsanız, belleğinizin sizi aldatmadığını söyleyebilirim; Java'dan bildiğimiz bu ileti, Scala'da da hoş yazım amacıyla kullanılıyor. Tıpkı Java'da olduğu gibi, programcıların belli bir türe ait değerlerin hoş yazımı için kök sınıf tarafından sağlanan ilişkin metot gerçekleştirimini ezmesi gerekiyor. Ancak, toString
'in Scala tür sıradüzeni içinde nereden nasıl kalıtlandığını daha iyi anlamak için, Java'da olmayan bir üstkavramın tanıtılmasında yarar var: çeşni (İng., mixin). Kaynak kodda trait
anahtar sözcüğüyle karşılık bulan bu programlama kavramının anlaşılması, Java ve Scala türleri arasındaki etkileşimi daha iyi kavramak için de yardımcı olacaktır. O zaman, karşılaştırılabilirlik kategorisini tanımlayan scala.math.Ordered
çeşnisinin Scala'nın resmi sitesindeki gerçekleştirimine ve Kesir
sınıfına söz konusu çeşninin nasıl katıldığına göz atarak bu üstkavramı anlamaya çalışalım.Soysal olan
Ordered
çeşnisi, başlığındaki kalıtlama ilişkisinden de görülebileceği gibi, Java platformundaki karşılaştırılabilir nesnelerin kategorisini tanımlayan Comparable
arayüzünden kalıtlar. Scala türlerinin Java'dakileri geliştirebileceğine örnek oluşturan bu başlığın anlamı, işini compare
metoduna havale ederek gören compareTo
metodu gerçekleştiriminden de gözlemlenebilir. Böylece, Scala'da yazılan bir sınıfın nesneleri Java programları içinden de kullanılabilecektir.Ordered.scala
package scala.math trait Ordered[A] extends java.lang.Comparable[A] { def compare(sağ: A): Int def <(sağ: A): Boolean = (this compare sağ) < 0 def >(sağ: A): Boolean = (this compare sağ) > 0 def <=(sağ: A): Boolean = (this compare sağ) <= 0 def >=(sağ: A): Boolean = (this compare sağ) >= 0 def compareTo(sağ: A): Int = compare(sağ) } // Ordered[A] çeşnisinin sonu object Ordered { implicit def orderingToOrdered[T](x: T) (implicit ord: Ordering[T]): Ordered[T] = new Ordered[T] { def compare(sağ: T): Int = ord.compare(x, sağ) } } // Ordered tek-örneklisinin sonu
Ordered
çeşnisinin gövdesine baktığımızda, tanımlanan kategorideki iki Scala nesnesinin—ileti alıcı (this
) ve sağ
—karşılaştırılma sonucunu döndüren compare
metodunun gövdesi verilmeden sağlandığını görüyoruz. Bu durum, Scala derleyicisi tarafından söz konusu metodun soyut olarak ele alınacağı anlamını taşır; derleyicinin yaptığı bu varsayım yüzünden programcının ayrıca çeşniyi veya metodu soyut olarak nitelemesine gerek yoktur.Yukarıdaki kod parçasının ortaya koyduğu bir diğer önemli nokta, Scala'nın, Java'nın aksine, işleçlerin aşırı yüklenmesini—ya da, işin doğrusunu söylemek gerekirse, bu tür bir yanılsamayı yaratabilecek özellikleri—desteklediğidir. Öncelikle, tanımlayıcı adlarının oluşturulmasında kullanılan karakterler kullanageldiğimiz işleçlerin simgelerini de kapsayan daha geniş bir yelpazeden seçilebilir.4 Ayrıca, tek argüman alan iletiler işleç ortada sözdizimiyle de kullanılabilir. Örneğin
+
, topla
veya add
kadar geçerli bir tanımlayıcı adıdır. İsteyecek olursak, değişken/sabit veya ileti/metot adlarını topla
veya add
yerine +
olarak da seçebiliriz. Aynı zamanda, adı ne şekilde verilmiş olursa olsun, tek argüman alan iletileri, alıcı.ileti(arg)
yerine alıcı ileti arg
şeklinde de gönderebiliriz. Dolayısıyla, yukarıdaki this compare sağ
ifadesi this.compare(sağ)
ile eşdeğerdir.Kod parçamızda dikkat çeken bir diğer bölüm, tek-örneklimizin tanımında geçen
implicit
anahtar sözcüğüdür. Bu sözcük, söz konusu metodun, programcının açıkça kullanması dışında kimi zaman arka planda derleyicinin sentezlediği kod tarafından da çağrılabileceğini belirtir. Örneğimizde olduğu gibi dönüşüm amacıyla kullanılan bu tür metotlar, nesne tutacağını argüman türünden (Ordering
) eşlik eden türe (Ordered
) çevirir. Mesela, Ordering
çeşnisi tutacağıyla bir nesneye ileti gönderilmesi ve bu iletinin geçerli olmadığının anlaşılması durumunda, derleyici yukarıdaki ve benzeri metotlardan birini usulca kullanarak nesneyi başka bir açıdan görecek ve program hata vermeden devam edecektir.Tanımlanmış bir çeşni, bir diğer çeşni tarafından kalıtlanmak suretiyle geliştirilebileceği gibi, içerdiği soyut öğelerin sağlanması ve/veya bazı öğelerinin ezilmesi yoluyla bir sınıfa katılabilir. Her iki durum da
extends
seçilmiş sözcüğü ile ifade edilir. Ancak; sınıfın bir başka sınıftan kalıtlaması halinde, extends
üstsınıfı belirtmek için kullanılırken, sınıfa katılan çeşniler with
anahtar sözcüğü ile belirtilir. Çeşniler arası kalıtlama ilişkisinin çoklu olmasının yanısıra, bir sınıf birden çok çeşniyi gerçekleştirebilir.Ordered
çeşnisinin Kesir
sınıfı tarafından gerçekleştirilmesi aşağıda verilmiştir. Bu tanıma göre, yaratılacak Kesir
ve Kesir
'den kalıtlayan tüm sınıfların nesneleri karşılaştırılabilir nesneler kategorisine gireceklerdir. Bu özellik, Ordered
çeşnisinin Comparable
'dan kalıtlaması nedeniyle, söz konusu nesnelerin sadece Scala programlarında kullanılmaları halinde değil, Java ve diğer Java platformu dillerinde yazılmış programlar içinden kullanılmalarında da geçerli olacaktır. Mesela, Kesir
nesneleri ile doldurulmuş bir liste java.util
paketindeki Collections.sort
metodu ile sıralanabildiği gibi, scala.collection.immutable.List
sınıfının sort
metoduyla da sıralanabilir.
... import scala.math class Kesir extends Ordered[Kesir] { ... def equals(sağ: Kesir) = compare(sağ) == 0 def compare(sağ:Kesir) = { val fark = this - sağ if (fark._pay < 0) -1 else if (fark._pay > 0) 1 else 0 } // compare(Kesir): Int sonu ... def -(sağ: Kesir): Kesir = { val pay = _pay * sağ._payda - _payda * sağ._pay new Kesir(pay, _payda * sağ._payda).sadeleştir() } // -(Kesir): Kesir sonu ... } // Kesir sınıfının sonu
Kesir
sınıfındaki equals
metodunun Ordered
çeşnisindeki compare
ile uyumlu olacak şekilde ezilerek gerçekleştirildiği gözünüze çarpmıştır. Sakın ola ki, Java'yı iyi bilen biri olarak, bunun eşitlik denetimi işlecini (==
) etkilemeyeceğini düşünmeyin. Çünkü, Scala'da equals
ile ==
her zaman aynı şekilde çalışır: kök sınıftaki equals
gerçekleştiriminin bir sınıf tarafından ezilmesi ==
işlecinin de anlamını değiştirir. Eşitlik denetimine ek olarak aynılık denetimi isteyenlerin, eq
ve onun değillemesi olan ne
iletilerini kullanması tavsiye edilir.Bir sınıfa çeşni katılması nesnenin yaratıldığı noktada, dinamik olarak da gerçekleştirilebilir. Örnek olarak,
KesirEksik
sınıfının Ordered
çeşnisi katılmadan tanımlanmış olduğunu varsayalım. Bu takdirde, aşağıdaki kod parçasından da görebileceğiniz gibi, bu sınıfa ait [1/11 değerine sahip] bir nesne Java'daki adsız sınıflara benzer bir tanımla söz konusu çeşniye sahip kılınabilir.
val ks = new KesirEksik(3, 33) with Ordered[KesirEksik] { def compare(sağ: KesirEksik) = { val fark = this - sağ if (fark.pay < 0) -1 else if (fark.pay > 0) 1 else 0 } // compare(KesirEksik): Int sonu def equals(sağ: KesirEksik) = compare(sağ) == 0 } // Kesir'i geliştiren adsız sınıfın sonu
toString
'in soy ağacını öğrenmek için yola çıkmıştık, şimdi equals
ve arkadaşlarının da katılımı ile iş daha da karıştı, değil mi? Üzülmeyin, aşağıdaki kısmi tür sıradüzeninin açıklanması her şeyi yoluna koyacaktır. [Yatık yazılı türler çeşnileri, diğerleri ise sınıfları temsil ediyor.]Any
AnyRef ≡ java.lang.Object
- ... // Java'dan ithal ediilen bileşke türler
ScalaObject
- ... // Scala'da tanımlanan bileşke türler
AnyVal
Boolean
+scala.runtime.RichBoolean
Byte
+scala.runtime.RichByte
...
Unit
Java'daki bileşke tür ve ilkel tür ayrımı Scala'da bire bir karşılık bulmaz; ilkel türlerin ele alınışını C# diline benzeterek anlamak daha kolay olacaktır. Çünkü, arka planda işlemcinin desteklediği türlerden birine eşleştirilerek işlenen bu çeşit değerler, kaynak kod düzeyinde kalıtlanarak geliştirilemeyen—yani
final
—özel sınıfların nesneleri gibi ele alınır. Dolayısıyla, ilkel türden değerler de ileti alıcı konumunda kullanılabilir. Bunu akılda tutarak yukarıda verilen sıradüzenini açalım. Öncelikle, ister ilkel olsun isterse bileşke, tüm türlerin kökü ==
, !=
, equals
, toString
ve diğer temel iletileri içeren Any
sınıfına gider. Scala nesnesi olduğunun anlaşılması için ScalaObject
adlı bir gösterge çeşninin katıldığı bileşke türlü değerler, java.lang.Object
'tekilere ek olarak eq
ve ne
gibi Scala nesnelerine özel iletiler içeren AnyRef
sınıfında belirlenen sözleşmeye göre davranırlar. İlkel türlü değerler ise, ilişkin sınıfları işaretlemek için kullanılan AnyVal
çeşnisi katılmış sınıflara aittir. Buna ek olarak, Predef
tek-örneklisinde yapılan dönüşümler sayesinde, tüm ilkel tür değerler daha zengin bir arayüze sahipmiş gibi görünebilirler. Örnek olarak, 1'den 10'a tüm tamsayıları standart çıktıya yazan for (i <- 1 to 10) System.out.println(i)
komutunun perde arkasına bir göz atalım. Unuttuysanız hatırlatalım, tek argümanlı ileti gönderileri işleç ortada sözdizimiyle de yazılabilir. Dolayısıyla, ileti alıcıdan başlayarak argümanındaki değere kadar olan tamsayıları içeren bir dilim döndüren to
iletisini dönüştürerek döngümüzü for (i <- 1.to(10)) System.out.println(i)
şeklinde yazmak da aynı işi görecektir. Yani, Int
türlü 1'e to
iletisi gönderilecek ve döndürülen dilim nesnesi gezilerek döngü işlenecektir. Ne var ki, Int
sınıfının arayüzüne bakıldığında, to
adında bir iletinin olmadığı görülecektir. O zaman, nasıl oluyor da Scala derleyicisi böyle bir durumda itiraz etmeden yukarıda anlatılan şeyi yapıyor? Bu sorunun yanıtı, Predef
tek-örneklisinde sağlanan dönüştürücü metodun örtük çağrısında (İng., implicit call) yatıyor: derleyici, iletinin desteklenmediğini görmesinin ardından Int
türlü hedef nesneyi scala.runtime.RichInt
nesnesine çeviriyor ve iletiyi bu nesneye gönderiyor. Bir diğer deyişle, Int
sınıfındaki işlevsellik RichInt
sınıfında sağlanan işlevsellikle zenginleştiriliyor.Bu haliyle çeşnilerin soyut sınıflardan çok da farklı olmadığını düşünebilirsiniz: tıpkı soyut sınıflarda olduğu gibi, çeşniler altalan tanımlarının yanısıra iletiler ve bu iletilerin bazıları veya tümü için gerçekleştirimler içerebilir. Buna karşılık, çeşniler yardımcı yapıcılara sahip olamaz; ek olarak, birincil yapıcılar argüman alamadığı gibi kalıtladıkları türlerin yapıcılarına argüman geçiremez. Ayrıca, (soyut) sınıflar için tekli kalıtlama geçerliyken çeşniler birden çok çeşniden kalıtlayabilir. Bundan dolayı bazılarınız, önceki paragraflardaki kategori sözcüğünün kullanımının da katkısıyla, çeşnilerin biraz da arayüzlere benzediğini düşünebilir. İki grubun da bir yere kadar haklı olduğu söylenebilir. Kimin haklı olduğunu ilan etmeden önce, Scala derleyicisinin çoklu sınıf kalıtlamanın desteklenmediği JSM üzerinde nasıl olur da soyut sınıf özellikleri sergileyen çeşnilerin çoklu kalıtımını olduruyor, ona bir bakalım. Bunun için sınıf dosyalarını tanımlanan türün üyelerini listeleyerek tersine bütünleştiren
javap
komutunu ve Scala derleyicisinin bir opsiyonunu kullanacağız.Çeşni.scala
trait Çeşni { def ileti() { println("iletide ...") } }
$ scalac Çeşni.scala $ ls Çeşni*.class Çeşni.class Çeşni$class.class $ javap Çeşni Compiled from Çeşni.scala public interface Çeşni extends scala.ScalaObject { public abstract void ileti(); }Çeşni.class dosyasının incelenme sonucu, tercihini arayüzden yana kullananları haklı çıkarmış gibi gözüküyor;
javap
komutunun çıktısına göre, tanımlamakta olduğumuz iletinin imzası arayüze aynen taşınmış. Ancak, doğal olarak, arayüzlerin gerçekleştirim ayrıntısı içerememesi nedeniyle, metot gövdesi uçup gidivermiş. Bu sihirbazlığa açıklık getirilmesi gerekli. İşte bu noktada, Scala derleyicisine geçirilecek print
opsiyonu yardım çağrımıza yanıt verecektir. $ scalac -print Çeşni.scala [[syntax trees at end of cleanup]]// Scala source: Çeşni.scala package <empty> { abstract trait Çeşni extends java.lang.Object with ScalaObject { def ileti(): Unit }; abstract trait Çeşni$class extends { def ileti($this: Çeşni): Unit = scala.this.Predef.println("ileti içinde ..."); def /*Çeşni$class*/$init$($this: Çeşni): Unit = { () } } }Görünen o ki, metot gövdesi yukarıda yaptığımız sorgu sonrasında listelenen ikinci dosyaya, Çeşni$class.class, taşınmış. Yani, ortada kaybolup giden bir şey yok. Ama,çeşnimizin dönüşümünü Scala üstkavramları cinsinden veren bu çıktıda açıklama getirilmesi gereken bir nokta var:
Çeşni$class
çeşnisi, Java'ya nasıl çevrilecek? Çok bekletmeden yanıtını verelim: soyut sınıf olarak. Her şeyi bir arada gösteren aşağıdaki kod üzerinden anlamaya çalışalım.
trait Çeşni { def ileti() = println("ileti içinde...") }
⇓ Scala → Java
public interface Çeşni { public abstract void ileti(); }
+
public abstract class Çeşni$class { static void ileti1(Çeşni $this) = { ... } ... }
class ÇeşniciBaşı extends Çeşni { ... }
⇓ Scala → Java
public class ÇeşniciBaşı implements Çeşni { ... void ileti() { Çeşni$class.ileti(this) } ... }Özetleyecek olursak; çeşni tanımının sözleşmesi bir arayüze, gerçekleştirim ayrıntıları ise bir soyut sınıfa konulur; çeşninin katıldığı sınıfta ise çeşninin arayüzündeki iletilere karşılık gelen metotlar, işlerini soyut sınıftaki gerçekleştirime havale ederek görürler.
Dikkatinizi çekeceğimiz son nokta, çeşni katılmış bir sınıfın nesnesine gönderilen iletilerin işlemesi sırasında
super
anahtar sözcüğünün anlamını ilgilendiriyor. Tekli sınıf kalıtlamanın geçerli olduğu ve arayüzlerin gerçekleştirim ayrıntısı içermediği Java'da üstsınıftaki yapıcı veya diğer metotlara atıfta bulunmak için kullanılan bu sözcük, çoklu çeşni kalıtlama, bir sınıfa birden çok çeşni katılabilmesi ve çeşnilerin gerçekleştirim ayrıntısı içermesi nedeniyle Scala'da anlaşılması daha zor bir anlama sahiptir. Programming in Scala kitabındaki örnek ile anlamaya çalışalım.class Hayvan trait Tüylü extends Hayvan trait Bacaklı extends Hayvan trait DörtBacaklı extends Bacaklı class Kedi extends Hayvan with Tüylü with DörtBacaklı
super
çağrılarının etkisini anlamak için, Scala'nın sıradüzenindeki türleri nasıl sıraya dizdiğini anlamak gerekir. Doğrusallaştırma adı verilen bu işlemin üç temel kuralı vardır:
- Bir sınıf üstsınıfları ve katılan çeşnilerinin öncesinde doğrusallaştırılır.
- Doğrusallaştırılmada daha önceden geçmiş bir tür bir daha sıraya konmaz.
- Bir sınıfın üstsınıfı ve birden çok çeşnisi olması durumunda, ilk olarak en son katılan çeşni doğrusallaştırılır.
Hayvan
sınıfından başlayalım. Birinci madde gereği, doğrusallaştırma sonucu türler Hayvan
, AnyRef
, Any
şeklinde sıralanacaktır. Yani, Hayvan
sınıfının içinde geçen bir super
çağrısı, AnyRef
sınıfı içindeki ilişkin metoda atıfta bulunacaktır. Hayvan
'ı geliştiren Tüylü
çeşnisinin doğrusallaştırılması ise, kendisi ve Hayvan
'ın doğrusallaştırılması ile elde edilen Tüylü
, Hayvan
, AnyRef
, Any
sırasını verecektir. Bacaklı
ve DörtBacaklı
için de benzer bir şekilde oluşturulan sıralama, sırasıyla, Bacaklı
, Hayvan
, AnyRef
, Any
ve DörtBacaklı
, Bacaklı
, Hayvan
, AnyRef
, Any
olarak bulunacaktır. Hayvan
sınıfına doğrudan ve Tüylü
ve DörtBacaklı
çeşnileri üzerinden olmak üzere üç değişik yoldan ulaşan Kedi
sınıfı, ikinci ve üçüncü maddeler akılda bulundurularak doğrusallaştırılmalıdır. Yani, istediğimiz sıra Kedi
sınıfının DörtBacaklı
, Tüylü
ve Hayvan
'ın doğrusallaştırılması ile birleştirilmesi sonucunda bulunacaktır. DörtBacaklı
'nın doğrusallaştırılması ile elde edilen sıranın
öteki türlerin sıralamasındaki tüm türleri içermesi nedeniyle, sonuç Kedi
, DörtBacaklı
, Bacaklı
, Tüylü
, Hayvan
, AnyRef
, Any
olarak ortaya çıkacaktır. Bu sıra, zincirleme super
çağrılarının bulunduğu bir kodda bize denetim akışının izleyeceği yolu verir.- Değinilecek farklılıklardan çoğunun kaynak kodu kısaltıp okumayı kolaylaştırdığına ve alana özel diller geliştirirken gerekli olacak akıcı arayüz yazımını olanaklı kıldığına dikkatinizi çekerim. ↑
- Yazıyı okurken Scala'nın kimi özelliklerini gösterebilmek adına, kodumuzu sınıf yazma reçetesinin anlatıldığı yazıda🔎 tavsiye edilenden farklı bir biçimde oluşturduğumuzu aklınızdan çıkarmayınız. ↑
- Hatırlayacak olursanız,
protected
niteleyicisi Java'da altsınıfların yanısıra, aynı paketteki türlere de erişim hakkı sağlar. ↑ - Tanımlayıcı adının oluşturulmasında işleçler ve diğer karakterlerin birbirine karıştırılarak kullanılması mümkün değildir. ↑