14 Ekim 2011 Cuma

Java Platformunun Dikkate Değer Bir Dili: Scala-2


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

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 sonu
Java'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 sonu
Birincil 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:
  1. Bir sınıf üstsınıfları ve katılan çeşnilerinin öncesinde doğrusallaştırılır.
  2. Doğrusallaştırılmada daha önceden geçmiş bir tür bir daha sıraya konmaz.
  3. 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.
Anlamanızı kolaylaştırmak için yalın bir tanımı olan 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.


  1. 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.
  2. 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.
  3. 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.
  4. 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.

Hiç yorum yok:

Yorum Gönder

Not: Yalnızca bu blogun üyesi yorum gönderebilir.