Język Scala dla programistów PHP pt.3 – traits

Kolejny odcinek serii o języku Scala zajmie się kwestią traits – cech. Jest to o tyle ciekawa część języka, że w PHP również zostaną one wprowadzone w najbliższej wersji 5.4 . Traits w PHP zainspirowane zostały właśnie bezpośrednio implementacją z Scali.

Czym jest trait ? Trait jest elementem języka służącym do zapewnienia dziedziczenia poziomego. Co oznacza dziedziczenie poziome ?

Ażeby dowiedzieć się czym jest dziedziczenie poziome, najpierw musimy ustalić czym jest dziedziczenie pionowe. Dziedziczenie pionowe, takie jakie znamy np. z PHP i Javy zakłada, że każda klasa potomna może dziedziczyć tylko z jednej klasy bazowej. Wszelkie metody i pola oznaczone przez „protected” obecne są też w klasie potomnej. Jednak co najważniejsze – klasa potomna jest podtypem klasy bazowej. Dziedziczenie oznacza, że klasa B jest również typu A .Ilustruje to poniższy przykład hierarchii klas znany z lekcji biologii:

Zwierzęta -> Strunowce -> Kręgowce -> Ssaki -> Walenie -> Delfiny -> Delfin Słodkowodny

Dziedziczenie poziome natomiast jest sytuacją gdy jakiejś klasie chcemy rozszerzyć funkcjonalność nie zaburzając wcześniej przedstawionej pionowej hierarchii klas. Weźmy dla przykładu naszego delfina i jego kolegę po miedzy – szympansa. Załóżmy, że chcemy zarówno delfina jak i szympansa nauczyć jakiś sztuczek np. salta w tył. Oczywiście moglibyśmy dodać metodę „salto w tył” abstrakcyjnej klasie „ssaki” z której dziedziczą szympans i delfin lub dodać kolejną klasę pośrednią. Natomiast pada pytanie jaki ma to sens ? W końcu tylko szympansy i delfiny umieją takie śmieszne sztuczki, natomiast inne ssaki nie są na tyle sprytne, żeby to wykonać. Generalnie wtłaczanie takiej cechy w dziedziczenie pionowe jest działaniem troszkę „na siłę”. Na szczęście mamy traity zwane też cechami. Traity w dowolnej ilości można „wczepić” do dowolnej klasy.

Traity często opisywane są jako interfejsy posiadające implementacje. Jest to dobre skojarzenie – implementacja przez jakąś klasę interfejsu oznacza, że posiada ona pewną funkcjonalność. Dołączenie traita do klasy znaczy dokładnie to samo, z tą różnicą, że trait może posiadać konkretną implementację.

Najwyższy czas pokazać, jak traity wykorzystywane są w praktyce w języku Scala. Trait, który przedstawię, będzie posiadał tylko jedną metodę – „toArray”. Będzie ona zwracała wszystkie zmienne istniejące w obiekcie w postaci obiektu klasy HashMap, który jest odpowiednikiem PHP-owego „arraya„. Przy okazji dowiemy się co nieco o refleksji oraz o domknięciach leksykalnych i iteracji.

Kod:


trait ToArray {
  def toArray: HashMap[String, Any] = {
    val vars = new HashMap[String, Any]()
    val className = this.getClass.getName

    def extractFields(className: String):HashMap[String, Any] = {
      val currentClass = java.lang.Class.forName(className)
      val superClass = currentClass.getSuperclass

      if(superClass == null){
        return vars
      }
      val fields = currentClass.getDeclaredFields

      fields.foreach((field:java.lang.reflect.Field) => {
          field.setAccessible(true)

          val name = field.getName
          val value = field.get(this)
          vars += name -> value

          field.setAccessible(false)
        })

      return extractFields(superClass.getName)
    }
    extractFields(className)

    return vars
  }
}

Metoda „toArray” jest funkcją zwracającą obiekt typu HashMap, gdzie klucz ma postać tekstową (String), natomiast wartość może być dowolnego typu (Any), jest ona również zadeklarowana jako stała – „vars„.

By pobrać nazwę klasy, w której znajduje się trait, używamy metody this.getClass, która zwraca nam Javowy obiekt reprezentujący klasę. Posiadający metodę, która zwraca nam jej nazwę „getName„. Następnie zdefiniowana jest funkcja extractFields, która jest jednocześnie domknięciem – importuje do swojego kontekstu stałą „vars„. Rekurencyjnie przemieszcza się ona w górę hierarchii klas i pobiera listę zadeklarowanych pól i dołącza je do stałej vars. Pragnąłbym zwrócić tutaj szczególną uwagę na sposób w jaki jest dołączany kolejny element do „arraya„. W PHP zrobilibyśmy coś takiego:

$vars[$name] = $value;

w Scali natomiast:

vars += name -> value

Kolejną ciekawym elementem jest metoda foreach wywoływana na liście pól klasy(Array). Pobiera ona jako argument dowolną funkcję i wywołuje ją na każdym elemencie, który zawiera, całkiem podobnie jak metoda each znana z jQuery.

Skoro w szczegółach omówiłem już kod naszego traita, czas pokazać, jak można go użyć. Są na to dwa sposoby. Pierwszy klasyczny – klasa Foo dziedziczy z traita ToArray.

class Foo extends ToArray{

  private var _bar = 5;

  def bar: Int = {
    return this._bar;
  }

  def bar_=(bar:Int) :Unit = {
    this._bar = bar;
  }
}

//Użycie
object Main {

  def main(args:Array[String]): Unit = {
    val foo = new Foo
    val arr = foo.toArray

    arr.foreach {
      tuple:(String, Any) =>
      val (key,value) = tuple
      println(key + " " + value)
    }
  }

}

Drugi sposób – inline’owe miksowanie:

class MyFoo extends Foo with ToArray

//Użycie

object Main {

  def main(args:Array[String]): Unit = {
    class MyFoo extends Foo with ToArray

    val foo = new MyFoo
    val arr = foo.toArray

    arr.foreach {
      tuple:(String, Any) =>
      val (key,value) = tuple
      println(key + " " + value)
    }
  }
}

Przy okazji drugiego przykładu wychodzi nam jeszcze jedna ciekawa własności Scali – obiekty (object), klasy (class) i cechy (traits) można również deklarować wewnątrz innych obiektów/klas/traitów oraz funkcji i metod. Mają one wtedy zasięg ograniczony do bloku kodu, w którym zostały zadeklarowane.

By artykuł był kompletny wymienię jeszcze, czym różni się trait od klasy:

  1. nie posiada konstruktora
  2. nie można stworzyć jego instancji

Mam nadzieję, że dzisiejszy odcinek przypadł Wam do gustu, a kod przetrawiliście jak szybką przekąskę na mieście. Już dziś zapraszam na kolejny odcinek poświęcony paczkom(package).

  1. singles pisze:

    Fajny wpis, ale jak to przy Scali, trzeba się wysilić przy czytaniu kodu 😉

    Aczkolwiek mam pytanie, które Tobie może wydawać się banalne, a równocześnie tyczy się także wielokrotnego dziedziczenia – mianowicie Diamond Problem.

    Mam twa traity, każdy implementuje tę samą metodę. Miksuje(?) oba traity w mojej klasie i wywołuję tę właśnie metodę. Która metoda zostanie wywołana? Muszę jawnie zdecydować?

  2. Zadałeś dobre pytanie. Szczerze powiedziawszy sam o tym nie pomyślałem. W każdym razie odpowiedź jest taka – tak trzeba jawnie powiedzieć, której metody chcemy użyć. Jak to zrobić -> http://stackoverflow.com/questions/1836060/cant-extend-two-traits-that-have-a-method-with-the-same-signature

  3. singles pisze:

    Czyli tak jak myślałem – wydaje mi się, że podobnie jest w Pythonie i jego super(). Z tego co widzę, w Rubym automatycznie bierze późniejszą deklarację.

  4. Theq pisze:

    Dobry wpis. Co do dodawania do HashMapy to należy wspomnieć, że tak to wygląda przy jej mutowalnej wersji.

    @”trait może posiadać tylko metody lub stałe (val)” Gdzie to znalazłeś? Mnie zmienne w traicie się kompilują.

  5. @theq – zmienne var – nie ale stałe – val tak, tutaj możesz wypróbować – http://ideone.com/vFYxY
    z HashMapą oczywiście masz rację, o niezmiennych strukturach danych powiem przy okazji eksploracji funkcyjnej natury Scali 😉

  6. Theq pisze:

    Nie bardzo rozumiem, czy dam val b = 1, czy var b = 1 to wynik jest ten sam.

  7. To widocznie można i jedne i drugie, widocznie się zagalopowałem. Pewnie myślałem o implementacji PHP-owej gdzie nie można zmiennych używać.

  8. Theq pisze:

    Najlepiej zerknąć co z traitami robi kompilator, np. w twoim przykladzie z wpisu wygląda to tak:

    class Foo extends java.lang.Object with ToArray with ScalaObject {
    /* … */

    def toArray(): HashMap = ToArray$class.toArray(Foo.this);

    /* … */
    }

    gdzie implementacja toArray (w klasie ToArray$class) wygląda tak:

    def toArray($this: ToArray): HashMap = {
    val vars$1: HashMap = new HashMap();
    val className: java.lang.String = $this.getClass().getName();

    /* … */

    return vars$1
    };

    I do tego klasa ToArray$class zawiera metodę /*ToArray$class*/$init$($this: ToArray) (super są te nazwy 😀 ), która jest wywoływana z konstruktora Foo, która zainicjuje nam zmienną z traita, która tak naprawdę będzie w klasie, która tego traita rozszerza.

  9. Widzę, że sięgnąłeś bardzo głęboko w kod. Ja nie mam aż takich ambicji, ale rozkmina dobra 😉

  10. mc pisze:

    Wydaje mi się, że pojęcie „dziedziczenie poziome” jest trochę nie semantyczne – bo klasy nie dziedziczą z cech, tylko posiadają cechy. Lepszym wyrazem jest agregacja:

    http://pl.wikipedia.org/wiki/Agregacja_(programowanie_obiektowe)

  11. mc pisze:

    *niesemantyczne 😉

  12. Może i tak, chociaż z drugiej strony, jak sobie sprawdzisz class.instanceOf i w argumencie podasz trait, to zwróci true. To znaczy, że nie jest tylko relacja agregacji a również tożsamości -> „is a”.

  1. There are no trackbacks for this post yet.

Leave a Reply

Informuj mnie o odpowiedziach poprzez e-mail. Możesz również subskrybować wpis bez zostawiania komentarza.