Scala etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
Scala etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

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.

6 Mayıs 2011 Cuma

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

Başlık yazının konusunu ele veriyor: sınıf dosyasına derlenmek suretiyle JSM tarafından çalıştırılabilir programlar üretmekte kullanılabilecek Scala dili. .NET'in CIL komutları içeren DLL formatına da derlenebilen, fonksiyonel programlama kavramlarını destekleyen bu nesne yönelimli dil, Java'ya eklenmesi düşünülen pek çok kavram için bir laboratuar ve esin kaynağı olma özelliği taşıyor. Scala programlama diline dair dizimizin ilk bölümü olan bu yazıda da bu dili Java bilenlere tanıtmaya çalışacağız.


Basit Bir Scala Programı


Java'da olduğu gibi, Scala'da da her şey bir sınıf içine yazılmak zorundadır. Aslına bakılırsa, ilkel türleri de sınıflarla temsil eden Scala bu konuda Java'dan daha katıdır diyebiliriz. Bir diğer farklılık, ';' karakterinin pek çok C-temelli dil tarafından kabul edilen komut sonlandırıcılık görevindeki değişikliktir. Satır sonlarının komut sonlandırma noktası olarak algılanması nedeniyle, birden fazla komutun aynı satıra konulmaları durumunda kullanılması dışında, Scala'da komut sonlarına ';' yerleştirilmesine gerek yoktur.

SelamMillet.scala
object SelamMillet {
  def main(args: Array[String]) =
    println("Selam millet!")
} // SelamMillet sınıfının sonu
Yukarıda verilen kaynak kodda main metodunun dönüş türünün eksik olduğunu düşünüyorsanız, Scala'nın bir diğer özelliğini söyleyerek yanıt verelim: Scala, ML ve Haskell programlama dillerinde olduğu gibi, program metninin sağladığı bilgilerden yararlanarak programlama öğelerinin türlerini çıkarsamaya çalışır. Bundan dolayı, Scala programcılarının derleyicinin çıkarsayamadığı yerler dışında hiçbir yere tür bilgisi koymasına gerek yoktur. Derleyici tarafından kabul görmekle birlikte, main metodunun imzasının aşağıdaki şekilde yazılması gerekmez.
def main(args: Array[String]): Unit = 
Örneğimizde sınıf kavramının izlerini arayanlar, kendilerini ikna etmekte zorlanabilirler. Zira, sınıf sözcüğü (class) yerini nesne sözcüğüne (object) bırakmıştır. Sınıfın bir şablon, nesnenin ise söz konusu sınıfın bir örneği olduğunu bilenleriniz bilgilerinden kuşku duymaya başladıysalar üzülmesinler, her şey eskiden olduğu gibi. Beklenen class sözcüğü yerine object sözcüğünün olması, tanımlanmakta olan sınıfın bir özelliğini yansıtmaktadır: Java terminolojisi ile açıklayacak olursak, SelamMillet sınıfının yegâne metodu olan main static'tir. Bir diğer deyişle, main metodu, SelamMillet sınıfının nesnesi yaratılmadan kullanılabilir. Nesne paradigması ile bağdaşmayan bu durum—problem çözümünde merkezi rol oynayan nesnenin bırakın kullanılmasını, yaratılması bile söz konusu değil—kimi zaman çalıştırılabilir sınıfın tek bir nesnesinin yaratılıp bu nesneye ileti gönderilmesi yoluyla aşılır. İşte, tasarım desenleri dünyasında tek örnek[li sınıf] (İng., singleton [class]) deseni olarak adlandırılan bu kullanım, Scala'da object anahtar sözcüğünün kullanımı ile belirtilir. Bu şekilde tanımlanmış sınıfların, tüm öğelerinin static olduğu varsayılır. Bundan dolayıdır ki, Scala'da static anahtar sözcüğüne yer yoktur, programcı bu tür özellikleri tek örnekli sınıflar içinde toplayarak bir object olarak tanımlamalıdır.

Kaynak kodun yazılması sonrasında yapılacaklar Java'daki ile aynıdır: ilk adımda sınıf dosyasına çevrilerek derlenen kod, JSM tarafından çalıştırılmalıdır.
# Derleme aşaması
$ scalac -encoding utf-8 SelamMillet.scala
$ ls SelamMillet*
SelamMillet.class   SelamMillet$.class   SelamMillet.scala
# Yorumlama aşaması
$ scala SelamMillet
Selam millet!
Yukarıdaki listelemenin iki noktası dikkatinizi çekmiştir. Öncelikle, Scala programlama dilinin derleyicisi olan scalac komutunun ürettiği iki tane sınıf dosyası vardır. Bunlardan, SelamMillet.class uygulama dosyası iken, SelamMillet$.class tek örnekli sınıf deseninin gerçekleştirildiği kodu içerir. İkinci nokta ise, JSM'nin java yerine scala komutuyla çağrılmasıdır. Bu, her programlama dili için JSM'nin ayrı ayrı gerçekleştirilmesi gerektiği yanılgısına neden olmamalıdır. Çünkü, scala komutu java komutunun Scala'ya has sınıf dosyalarını yükleyerek işini gören uyarlaması olarak düşünülebilir. Dolayısıyla, yorumlama aşması aşağıdaki şekilde de yapılabilir.
# Yorumlama aşaması
$ java -cp .:/usr/share/java/scala-library.jar SelamMillet
Selam millet!
Bunun güzel bir sonucu, çöp toplama, güvenlik denetimleri, anında derleme gibi JSM tarafından sağlanan pek çok hizmetin Scala programları için de otomatikman sağlanmasıdır; Scala dilinin (ve diğer JSM üzerinde çalışan dillerin) geliştiricilerinin bu hizmetleri ayrıca gerçekleştirmelerine gerek yoktur.

Java Programlama Dili Sınıflarının Kullanımı


JSM dili olmaları nedeniyle, gerek Java gerekse Scala, kurulumunuzun sınıf yolu üzerindeki tüm sınıf dosyalarını, kaynak kodları hangi dilde yazılmış olursa olsun, kullanabilir. Scala programcısı açısından bakıldığında bu, Scala kaynak kodu içinden Java platformunun bildik tüm işlevselliğinden sanki Scala'da yazılmış gibi yararlanılabileceği anlamına gelir. Bu, aşağıdaki örnekte verildiği gibi işlevselliği kullanmak şeklinde olabileceği gibi, platformdaki bir sınıftan kalıtlama veya bir arayüzü gerçekleştirme şeklinde de olabilir. Ancak, Java'dan gelip Scala kaynak kodu üretenlerin yararlanılacak işlevi Scala sözdizimi ile kullanmaları gerektiğini akılda tutmaları gerekir. Aşağıdaki örnek üzerinden görelim.1

Açıklamalara Scala'nın Java ile farklılık gösteren bir özelliğine dikkat çekerek başlayalım: dosya içinde dosya ile aynı ada sahip bir sınıfın bulunması gerekmez; Scala'da böyle bir zorunluluk yok. Ancak, daha sonraki derleme/çalıştırma komutlarında örneği verildiği gibi, derleyiciye geçirilenin dosya adı iken yorumlayıcıya geçirilenin sınıf dosyasının adı olduğu unutulmamalıdır.

Örneğimizin Scala'ya yeni gelenler için dikkat çeken bir yönü, import bildiriminin ilk satırdaki seçimli kullanımı.2 Tekli ve jokerli3 kullanımlara sahip bu bildirim, örneğimizde de olduğu gibi, bir veya daha fazla sayıda türün görünür kılınmasında da kullanılabilir. Dikkat çekecek bir diğer anahtar sözcük ise, simgesel sabit tanımlamak için kullanılan val.4

Örnek2.scala
import java.util.{Scanner, Vector}

object ScaladanJava {
  val grdKnl = new Scanner(System.in);
  println("İkinci Scala örneğine hoş geldiniz...")

  def main(args: Array[String]) = {
    val tekSayılar = new Vector[Int]()
    print("Artı bir tamsayı giriniz: ")
    val j = grdKnl.nextInt()
    for(i <- 1 to j by 2) tekSayılar.add(i)
    println("1 ile" + j + " arasındaki tek sayılar: " + tekSayılar)
  }

  println("Bu örnek Scala kodundan Java'da yazılmış işlevselliği kullanmaya dönük")
} // ScaladanJava sınıfının sonu
Metot gövdeleri dışındaki komutların varlığı da sizi telaşa sürüklemesin; bu komutların sınıf içindeki sıraları korunacak şekilde sınıf ve/veya nesne ilkleme bloğu içine konulduğunu düşünmeniz işinizi kolaylaştıracaktır. Tek örnekli bir sınıf olması nedeniyle tüm öğeleri static varsayılan sınıfımızda, metot tanımları dışındaki her şey sınıf ilkleme bloğuna konulup uygulamanın çalıştırılması sonrasında ilk iş olarak işlenirken, new işleci ile nesnesi yaratılabilen çok örnekli sınıflarda bu tür öğeler nesne ilkleme bloğuna konulacak ve sentezlenen bu nesne ilkleme bloğu her nesne yaratılışı noktasında yapıcı çağrısı öncesinde işlenecektir.
$ scalac -encoding utf-8 Örnek2.scala
$ scala ScaladanJava
İkinci Scala örneğine hoşgeldiniz...
Bu örnek Scala kodundan Java'da yazılmış işlevselliği kullanmaya dönük
Artı bir tamsayı giriniz: 16
1 ile 16 arasındaki tek sayılar: [1, 3, 5, 7, 9, 11, 13, 15]

Diziler Soysal Bir Türün Örneğidir


Verdiğimiz örneklerde komut satırından geçirilen argümanları tutmak için kullanılan dizi ve ikinci örneğimizdeki Vector nesnesinden de görülebileceği gibi, Scala'da soysallık tür parametrelerine karşılık gelen tür argümanlarının köşeli ayraç çifti arasında geçirilmesiyle sağlanır. Buna göre Vector[Int], Int türlü elemanlara sahip bir kabın kullanılacağını gösterir. Bu tanım sonrasında, söz konusu kap parametrik türün (java.util.Vector) sağladığı tüm işlevsellikten yararlanarak manipüle edilebilir. Bu, soysal Array türünün örneği olan diziler için de geçerlidir. Tanımlanan bir dizi, Array sınıfınca sunulan tüm işlevsellikten yararlanabilecektir.


  1. Gözünüzden kaçmış olabilir, işaret etmekte yarar var: ilk örneğimizin aksine ikinci örneğimizin metot gövdesi kıvrımlı ayraç çifti arasına yazılmıştır. Bunun sebebi, tek komuttan oluşması durumunda Scala'daki metot gövdelerinin, tıpkı Java'daki if, while yapılarının gövdeleri gibi, kıvrımlı ayraç çifti arasına yazılmalarının zorunlu olmamasıdır.
  2. java.lang paketindeki türler, herhangi bir bildirime gerek olmadan Scala derleyicisi tarafından görünür hale getirilirler.
  3. Söz konusu paketin tümünü görünür kılan jokerli kullanım, Java'da "*" ile belirtilirken Scala'da '_' ile belirtilir.
  4. Tanımlayıcının güncellenebilir bir içerik tutabilmesi için val yerine var ile nitelenmesi gerekir.