Programowalny ketchup – operatory

Jak można nazwać języka jak ketchup? Ano można, a dziś pierwszy wpis na temat Kotlina, czyli języka od JetBrains działającego na JVM. Przez pewien czas zastanawiałem się jak podejść do tematu i co na początek pokazać, żeby było ciekawie. Z jednej strony są data class, które są fajne. Z drugiej specyficzne modyfikatory dostępu, które w świecie JVM mają sens. Jednak koniec końców stwierdziłem, że zacznę od czegoś co w Javie nie występuje, ale było by bardzo przydatne, czyli od przeciążania operatorów.

Problem wersjonowania

Załóżmy, że mamy sobie klasę Report, która trzyma sobie wersję, która to wersja będzie podnoszona i tylko podnoszona. W javie realizacja tego zadania polega najprościej na wprowadzeniu klasy Version (bo gołe longi są chujowe), która będzie niosła w sobie odpowiednie informacje.

Kotlin pozwala na zrobienie tego trochę inaczej. Na początek zdefiniujmy sobie data class (taki semantyczny value object) Version, która będzie miała tylko jedno pole:

Listing 1. Klasa Version w Kotlin

data class Version(val version: Long) {

    init {
        if (version < 0) throw IllegalArgumentException("Version - ${version} - is less than 0");
    }
}

Teraz chciałbym móc w jakiś łatwy sposób podbijać numer wersji. Można dodać metodę tak jak w Javie, ale po co skoro można przeciążyć operator? Mechanizm przeciążania operatorów w Kotlinie jest dość prosty. Jest lista zdefiniowanych funkcji, które należy dopisać do klasy w odpowiedni sposób i już mamy możliwość użycia go w naszym kodzie. Dorzućmy wiec do naszej klasy wsparcie dla operatora +a:

Listing 2. Klasa Version z operatorem

data class Version(val version: Long) {

    init {
        if (version < 0) throw IllegalArgumentException("Version - ${version} - is less than 0");
    }

    operator fun unaryPlus(): Version {
        return copy(version + 1);
    }
}

Metoda unaryPlus pozwala na użycie operatora + w stosunku do naszego obiektu. Przykładowy test sprawdzający nasz pomysł:

Listing 3. Test działania operatora + w klasie Version

class VersionTest() {

    @Test
    fun shouldUnaryPlusIncreaseVersionByOne(){
        val v0:Version = Version(0);
        val v1:Version = +v0;

        Assertions.assertThat(v1.version).isEqualTo(1L);
        Assertions.assertThat(v1).isNotSameAs(v0);
    }
}

Wszystko ładnie, ale to nie jest tak oczywisty zapis jak użycie dobrze znanego ++. W tym celu dodajmy do naszej klasy kolejną metodę - inc:

Listing 4. Definiowanie operatora ++ w klasie Version

data class Version(val version: Long) {

    //...
  
    operator fun inc():Version{
        return copy(version + 1);
    }
}

Tyle tylko, że ten operator działa inaczej niż + co spowoduje mały problem.

Listing 5. Test działania operatora ++ w klasie Version

class VersionTest() {

    @Test
    fun shouldIncIncreaseVersionByOne(){
        val v0:Version = Version(0);
        val v1:Version = v0++;

        Assertions.assertThat(v1.version).isEqualTo(1L);
        Assertions.assertThat(v1).isNotSameAs(v0);
    }
}

Ten kod się nie skompiluje ponieważ operator ++ wartość zmiennej zatem zmieńmy val na var

Listing 6. Test działania operatora ++ w klasie Version

class VersionTest() {

    @Test
    fun shouldIncIncreaseVersionByOne(){
        var v0:Version = Version(0);
        val v1:Version = v0++;

        Assertions.assertThat(v1.version).isEqualTo(1L);
        Assertions.assertThat(v1).isNotSameAs(v0);
    }
}

i zobaczmy co się stanie:

Listing 7. Wyniki testu

org.junit.ComparisonFailure: 
Expected :1L
Actual   :0L

I jest to rozsądne ponieważ najpierw przypisaliśmy wartość do v1 równą v0, a potem podbiliśmy v0. Czyli wiemy już jak działa postinkrementacja w kotlinie (jak działa w Szepczącym Lesie? oto jest pytanie). Zmieńmy nasz kod na preinkrementację:

Listing 8. Preinkrementacja

class VersionTest() {

    @Test
    fun shouldIncIncreaseVersionByOne(){
        var v0:Version = Version(0);
        val v1:Version = ++v0;

        Assertions.assertThat(v1.version).isEqualTo(1L);
        Assertions.assertThat(v1).isNotSameAs(v0);
    }
}

Odpalamy i otrzymujemy

Listing 9. Wyniki testu preinkrementacji

java.lang.AssertionError: expected not same:<Version(version=1)>

I to też jest zrozumiałe ponieważ najpierw podbiliśmy v0, a następnie przypisaliśmy wynik do v1. Czyli preinkrementacja działa też w rozsądny sposób. Zatem nasz kod testu w wersji finalnej będzie wyglądał tak

Listing 10. Działajkacy test

class VersionTest() {

    @Test
    fun shouldIncIncreaseVersionByOne(){
        var v0:Version = Version(0);
        val v1:Version = v0;

        v1++

        Assertions.assertThat(v0.version).isEqualTo(0L);
        Assertions.assertThat(v1.version).isEqualTo(1L);
        Assertions.assertThat(v1).isNotSameAs(v0);
    }
}

Możemy tu zaobserwować ważną rzecz. Operator ++ (oraz --) dotyka tylko zmiennej na rzecz której został wywołany. Obiekt reprezentowany przez tą zmienną nie podlega podmianie chyba, że funkcja inc (albo dec) explicite zmienia coś w obiekcie. Oznacza to też, że funkcje te muszą zwracać obiekt o co najmniej tym samym typie co typ w którym zostały zdefiniowane, mogą oczywiście zwrócić podtyp.

W przypadku operatora +a (i innych) możemy zwrócić cokolwiek. Reszta w dokumentacji.

Podsumowanie

Kotlin pozwala na przeciążanie istniejących operatorów. To duży plus ponieważ pozwala to na uproszczenie zapisu pewnych fragmentów kodu. Jednocześnie nie możemy definiować własnych operatorów np. próba zdefiniowania :: albo nie przejdzie. Jest to zachowanie pośrednie pomiędzy czystą Javą gdzie nie można było przeciążać operatorów, a Scalą gdzie można przeciążać operatory jak i definiować nowe (technicznie są to "dziwne" nazwy metod).

3 myśli na temat “Programowalny ketchup – operatory

Napisz odpowiedź

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax