Scala – SWT i DSL

Motywacja

No i stało się. Poszedłem za radą Xiona i zerknąłem bliżej na Scalę. Przez ostatnie tygodnie trochę o niej poczytałem i napisałem kilka niewielkich rzeczy dla nabrania wprawy ale pisanie zwykłego kodu w Scali (poza składnią) nie różni się mocno od dowolnego innego języka (choć to też zależy od tego czy chce się pojechać po bandzie z możliwościami funkcyjnymi Scali 🙂 )

W trakcie zabawy natknąłem się na różne artykuły odnośnie tworzenia Domain Specific Language (DSL). Wówczas nie zwróciłem na to uwagi dlatego, że wykorzystywane tam konstrukcje wydawały się pochodzić zupełnie z innego świata. Ostatnio pomyślałem, że prędzej czy później trzeba będzie wypłynąć na głęboką wodę i się tym zająć. Lepiej więc wcześniej niż później 🙂

Pierwsze próby

Chcąc upiec dwie pieczenie na jednym ogniu zdecydowałem się na implementację DSL dla systemu GUI SWT. Oczywiście na początku nie wiedziałem zupełnie czego chcę ani tym bardziej – jak to osiągnąć. Dlatego postąpiłem tak jak każdy, kto osiąga taki punkt swojego życia – zapytałem Google…

Po kilku mało interesujących przykładach natrafiłem na coś wartościowego. Pomysł jest bardzo interesujący i pokazał mi w którą stronę można się udać. Na podstawie informacji o szczegółach implementacji, które zostały tam pokazane postanowiłem odtworzyć ten system i po około godzinie prób i błędów miałem już okienko i kilka przycisków na ekranie.

Pomysł z wykorzystaniem kilku dwóch list parametrów (jednej do podania listy funkcji ustawiających daną kontrolkę i drugiej określającej rodzica kontrolki) jest dosyć ciekawy. Jedyna rzecz jaka mi przeszkadzała to pewien brak porządku. Wywołania funkcji, które określają zachowanie kontrolki nadrzędnej mogą być poprzeplatane deklaracjami kontrolek dzieci:

group (
  text("Name"),
  gridLayout(2, false),
  gridData(FILL, FILL, true, true),
  label(text("First")), edit(text("Bullet")),
  label(text("Last")), edit(text("Tooth"))
),

W powyższym przykładzie text, gridLayout oraz gridData dotyczą elementu group natomiast wszystkie label i edit to już definicje nowych obiektów.

I wreszcie coś swojego

Stwierdziłem, że spróbuję napisać to jakoś inaczej. I po kilku godzinach kombinowania i eksperymentowania z różnymi aspektami języka udało mi się napisać działający kod, który generuje takie samo okienko jak w powyższym wpisie:

val shell = new SwtBuilder {
  shell text "User Profile" gridLayout (2, true)
  group text "Name" gridLayout 2 gridData (SWT.FILL, SWT.FILL, true, true) withChildren {
    label text "First";
    edit text "Bullet" gridData (SWT.FILL, SWT.CENTER, true, true) name "first"
    label text "Last";
    edit text "Tooth" gridData (SWT.FILL, SWT.CENTER, true, true) name "last"
  }
  group text "Gender" fillLayout (SWT.HORIZONTAL, 5) gridData (SWT.FILL, SWT.FILL, true, true) withChildren {
    radio text "Male"
    radio text "Female"
  }
  group text "Role" fillLayout (SWT.HORIZONTAL, 5) gridData (SWT.FILL, SWT.FILL, true, true) withChildren {
    checkbox text "Student"
    checkbox text "Employee"
  }
  group text "Experience" fillLayout (SWT.HORIZONTAL, 5) gridData (SWT.FILL, SWT.FILL, true, true) withChildren {
    spinner
    label text "years"
  }</p>
<p>  button text "Save" gridData (SWT.RIGHT, SWT.CENTER, true, true) onSelect {
    (ctls get "first", ctls get "last") match {
      case (Some(f:Text), Some(l:Text)) =>
        println("Name: " + f.getText + " " + l.getText)
      case _ => println("Something is wrong!")
    }
  }
  button text "Close" gridData (SWT.LEFT, SWT.CENTER, true, true) onSelect {
    shell close
  }
} shell

Wynik działania powyższego kodu

Część tajemnicy tkwi w tym, że scala zezwala na wywoływanie metod z argumentami bez kropki:

val label = new Label(parent, SWT.NONE)
label.setText("Hello World") // te dwie linie
label setText "Hello World"  // dają ten sam efekt

Pozostała część twki konwertowaniu w locie obiektów SWT do różnych wrapperów dodających wszelkie metody typu text lub gridData. Funkcje tworzące nowe kontrolki (label, button itp.) tak na prawdę tworzą jedynie nowe obiekty danej klasy:

def button() : Button = new Button(context.value, SWT.PUSH)
def button(style:Int) : Button = new Button(context.value, style)

Jeśli w tym momencie pomyślałeś, że zamiast pisać funkcję dwa razy z różnymi parametrami można ją napisać raz z parametrem domyślnym to… nie 🙂 Zależy mi na tym, żeby w kodzie było jak najmniej bezsensownych nawiasów. Jeśli utworzę tylko jedną wersję funkcji to jeśli nie chcę podać stylu musiałbym dodać parę pustych nawiasów – paskudne!

Wracając do rozwiązywania tajemnicy działania… W klasie SwtBuilder zdefiniowane są klasy typu:

class SetTextWrapper[T <: {def setText(text:String)}](val subject:T) {
  def text(t:String) : T = {
    subject.setText(t)
    return subject
  }
}

Taka klasa bierze jako argument konstruktora dowolną klasę typu T, który posiada metodę setText(text:String) i pozwala wywołać na niej swoją metodę text, która ustawia tekst w obiekcie klasy T. Kluczowe jest tutaj zwracanie oryginalnego obiektu, a powód wyjaśni się za chwilę. Do pary potrzebujemy jeszcze automatycznego konwertera, który będzie używany przez kompilator kiedy ten uzna, że konwersja jest potrzebna:

implicit def convertToSetTextWrapper[T <: {def setText(text:String)}] (t:T) = new SetTextWrapper(t)

To mówi kompilatorowi, że wszystkie obiekty posiadające posiadające typ T mogą być automatycznie (słowo kluczowe implicit) owinięte obiektem klasy SetTextWrapper. Dzięki takiej konstrukcji możliwe jest napisanie:

(new Label(parent, SWT.NONE)).text("Test").text("Hello").text("World")

Zaczynając od początku, mamy obiekt klasy Label. Następnie kompilator spotyka wywołanie metody text, której taki obiekt nie posiada. Wówczas używając zdefiniowanej wyżej funkcji konwertującej owija obiekt w SetTextWrapper i teraz może już spokojnie wywołać metodę text. Ta z kolei zwraca znów obiekt typu Label i historia zaczyna się od początku dla następnego wywołania text.

Biorąc pod uwagę, że mamy zdefiniowaną metodę label(), która generuje nowy obiekt typu Label oraz to, że scala pozwala pisać bez kropek i nawiasów możemy napisać powyższą linię kodu bardziej podobnie do pierwszego przykładu:

label text "Test" text "Hello" text "World"

Oczywiście nic nie stoi na przeszkodzie aby używać pierwszej notacji. Ma ona tę zaletę, że można rozpisać właściwości obiektu na kilka linii zamiast pisać jedną strasznie długą:

group()
  .text("Name")
  .gridLayout(2)
  .gridData(SWT.FILL, SWT.FILL, true, true)
  .withChildren {
    label text "First";
    edit text "Bullet" gridData (SWT.FILL, SWT.CENTER, true, true) name "first"
    label text "Last";
    edit text "Tooth" gridData (SWT.FILL, SWT.CENTER, true, true) name "last"
  }

A jak działa withChildren?

O! Cieszę się, że pytasz 🙂 Otóż… działa na takiej samej zasadzie jak to co opisano powyżej:

class CompositeWrapper[T <: Composite](c :T) {
    def withChildren(f: => Unit) : T = {
      context.withValue(c) { f }
      return c
    }
  }

Mamy więc pewien wrapper, który jako argument konstruktora przyjmuje obiekty typu T, które tym razem są obiektami dziedziczącymi (pośrednio bądź bezpośrednio) po typie Composite. Łatwo się domyślić, że typ ten pozwala na pokazywanie kontrolek „w sobie” czyli może być on rodzicem np. dla obiektu klasy Button. Do pary oczywiście jest odpowiednia metoda konwertująca ale wygląda analogicznie to pokazanej wcześniej więc jej nie pokażę.

Jak widać wrapper posiada jedną metodę withChildren, która przyjmuje jako parametr funkcję bezargumentową, a zwraca znów typ T. Szalenie interesujący jest tutaj obiekt context. Jest to obiekt klasy DynamicVariable, który za pomocą metody withValue pozwala w czasie wykonania bloku kodu podanego jako drugi argument podmienić wartość context.value na wartość podaną jako pierwszy argument. Zmienna context.value to oczywiście aktualnie używany rodzic dla wszystkich kontrolek w danym bloku. Zatem jeśli mam zdefiniowaną metodę:

def label() : Label = new Label(context.value, SWT.NONE)

I wykonamy kod:

context.withValue(new Composite(otherParent, compositeStyle)) {
  label text "First"
  label text "Second"
}

To powstaną dwa obiekty Label oraz komponent Composite, który jednocześnie będzie rodzicem dla tych dwóch etykiet.

Ok, a co ze zdarzeniami?

Kolejne świetne pytanie! Samo utworzenie interfejsu to jedynie część sukcesu. Przede wszystkim powinniśmy mieć możliwość jakoś dostać się do naszych kontrolek. Na tę okoliczność przygotowałem specjalny… wrapper 🙂 Wrapper ten działa dla wszystkich obiektów typu Widget (oczywiście ma

konwerter do pary). Pozwala na nadanie kontrolce nazwy i zapisanie jej w mapie obiektów pod daną nazwą (która jest obiektem Scalowej klasy Map).

W taki sposób możemy sobie spokojnie nazwać element, a po utworzeniu GUI w prosty sposób się do niego odwołać. Pozostaje jeszcze kwestia zdarzeń typu wciśnięcie przycisku. W SWT rejestruje się w danym obiekcie implementację interfejsu SelectionListener, i odbywa się to oczywiście dzięki metodzie addSelectionListener. Dlatego możemy utworzyć specjalny wrapper dla wszystkich obiektów posiadających tę metodę (oczywiscie razem z konwerterem):

class SetSelectionWrapper[T <: {def addSelectionListener(listener:SelectionListener)}](val subject:T) {
  def onSelect(f: => Unit) : T = {
    subject.addSelectionListener(new SelectionAdapter() {
      override def widgetSelected(e:SelectionEvent) : Unit = f
    })
    return subject
  }
}</p>
<p>implicit def convertToSetSelectionWrapper[T <: {def addSelectionListener(listener:SelectionListener)}](t:T) = new SetSelectionWrapper(t)

I teraz już nie ma żadnych przeszkód by napisać:

button text "Save" gridData (SWT.RIGHT, SWT.CENTER, true, true) onSelect {
  (ctls get "first", ctls get "last") match {
    case (Some(f:Text), Some(l:Text)) =>
      println("Name: " + f.getText + " " + l.getText)
    case _ => println("Nobody Expects The Spanish Inquisition!")
  }
}

Dodam tylko, że ctls to wspomniana wcześniej mapa nazwanych kontrolek.

Podsumowanie

Ogromną zaletą takiego rozwiązania jest duża elastyczność. Nic nie stoi na przeszkodzie aby w bloku kodu dla withChildren używać dowolnych konstrukcji języka – jest to w końcu zwykły kod. Dodatkowo, jakoby za darmo, kompilator w trakcie pracy wytknie nam wszystkie błędy (np. próbę ustawienia tekstu dla kontrolki, która tego nie obsługuje).

Cóż. Jak się okazuje – nie taki diabeł straszny jak go malują. Po przezwyciężeniu początkowej niechęci do składni Scali i poświęceniu sporej ilości czasu na zrozumienie o co tam w ogóle chodzi nauka nowych rzeczy przychodzi już dużo łatwiej a błędy kompilatora stają się twoimi przyjaciółmi 🙂

Reklamy

Slick

W jednym z poprzednich postów napisałem, że Slick udostępnia biblioteki natywne tylko dla Windowsa. Otóż dzisiaj przyjrzałem się temu dokładniej i okazało się, że po prostu Windowsowe nativy są w głównym katalogu natomiast pozostałe są popakowane w archiwa JAR i wrzucone do katalogu lib. Wcześniej ich po prostu nie zauważyłem.

W związku z poczynionym odkryciem uruchomiłem netbeansa na nowo zainstalowanym OpenSUSE i rozpocząłem poszukiwanie tutoriali w internecie. Tutaj niestety nie jest tak fajnie jak w GTGE. Dostępne są głównie przykładowe kody źródłowe. Udało mi się jednak znaleźć jeden tutorial ( http://thejavablog.wordpress.com/2008/06/08/using-slick-2d-to-write-a-game/ ), który pozwala załapać jak to mniejwięcej wygląda.

Prawdę mówiąc sam szkielet jest prawie taki sam jak to było w przypadku GTGE. Mamy metody init, update, render, których znaczenia i funkcji raczej nie trzeba tłumaczyć 😉

Slick jest pierwszym silnikiem 2D w Javie, w którym odkryłem możliwość dowolnego obracania obrazków. Dodatkowo przeglądając API zauważyłem, że posiada różne algorytmy wyszukiwania trasy, silnik cząsteczkowy (wraz z edytorem) oraz wsparcie dla tile-map rysowanych w programie Tiled.

Myślę że spędzę na tym silniku kilka ładnych godzin ponieważ zamierzam wziąć udział w tegorocznym WSOC! To chyba będzie pierwsza praca na WSOC napisana w Javie 😉

Object Oriented Programming

Czasem zaczynam myśleć, że programiści traktują zasady obiektowości jako święte i bez znaczenia jest czy to co tworzą będzie wygodne w użyciu. Przecież musi być zgodne z regułami OOP!

Ostatnio na forum widziałem gdzieś prawie odrazę dla stosowania zmiennych globalnych – pomimo tego ze 90% obiektów potrzebowała właśnie tej jednej zmiennej.

Chodzi mi o wskaźnik na urządzenie D3D. W temacie poruszany był problem umieszczenia tego w kodzie i jedyne rozwiązania widziane przez autora to:

  • wcisnąć wskaźnik do jednej klasy w sekcji private i chyba uczynić go jeszcze statycznym i zaprzyjaźnić z nią 3/4 innych klas
  • skutecznie przekazywać wskaźnik do urządzenia kolejnym klasom które go wymagają.

Nie trzeba być super programistą żeby domyślić się, że pierwsze rozwiązanie jest raczej naciągane przez duże `N`, a drugie po prostu nie wygodne.

A nie łatwiej/szybciej/elastyczniej jest po prostu dać ten wskaźnik jako zmienną globalną? Ja zastosowałem właśnie takie rozwiązanie w AGE. Zyskałem na tym łatwość w dodawaniu nowych części, które potrzebować będą tego wskaźnika, no i najważniejsze – wygodę. Nie mam miliona wskaźników tylko jeden z którego korzystają wszyscy oraz nie muszę przekazywać tego wskaźnika każdemu nowemu obiektowi z osobna.

IMHO reguły OOP to tylko dobre wskazówki i nie można ich traktować jako wyznacznika tworzenia nowego kodu… Przynajmniej ja nie będę dopóki rozwiązanie zgodne z OOP będzie strasznie niewygodne w późniejszym użyciu.