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

Jedna myśl nt. „Scala – SWT i DSL

  1. Pingback: Play Framework – Formatowanie JodaTime w templatach | Moriturius's Devlog

Skomentuj

Wprowadź swoje dane lub kliknij jedną z tych ikon, aby się zalogować:

Logo WordPress.com

Komentujesz korzystając z konta WordPress.com. Wyloguj / Zmień )

Zdjęcie z Twittera

Komentujesz korzystając z konta Twitter. Wyloguj / Zmień )

Zdjęcie na Facebooku

Komentujesz korzystając z konta Facebook. Wyloguj / Zmień )

Zdjęcie na Google+

Komentujesz korzystając z konta Google+. Wyloguj / Zmień )

Connecting to %s