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:
- nie posiada konstruktora
- 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).
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ć?
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
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ę.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ą.
@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 😉
Nie bardzo rozumiem, czy dam val b = 1, czy var b = 1 to wynik jest ten sam.
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ć.
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.
Widzę, że sięgnąłeś bardzo głęboko w kod. Ja nie mam aż takich ambicji, ale rozkmina dobra 😉
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)
*niesemantyczne 😉
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”.