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 
<p>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 <samp>+a</samp>:</p>
<p class="listing">Listing 2. Klasa <samp>Version</samp> z operatorem</p>kotlin
data class Version(val version: Long) {

    init {
        if (version 
<p>Metoda <samp>unaryPlus</samp> pozwala na użycie operatora <samp>+</samp> w stosunku do naszego obiektu. Przykładowy test sprawdzający nasz pomysł:</p>
<p class="listing">Listing 3. Test działania operatora <samp>+</samp> w klasie <samp>Version</samp></p>kotlin
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);
    }
}

<p>Wszystko ładnie, ale to nie jest tak oczywisty zapis jak użycie dobrze znanego <samp>++</samp>. W tym celu dodajmy do naszej klasy kolejną metodę - <samp>inc</samp>:</p>
<p class="listing">Listing 4. Definiowanie operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
data class Version(val version: Long) {

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

<p>Tyle tylko, że ten operator działa inaczej niż <samp>+</samp> co spowoduje mały problem.</p>
<p class="listing">Listing 5. Test działania operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
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);
    }
}

<p>Ten kod się nie skompiluje ponieważ operator <samp>++</samp> wartość zmiennej zatem zmieńmy <samp>val</samp> na <samp>var</samp></p>
<p class="listing">Listing 6. Test działania operatora <samp>++</samp> w klasie <samp>Version</samp></p>kotlin
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);
    }
}

<p> i zobaczmy co się stanie:</p>
<p class="listing">Listing 7. Wyniki testu</p>bash
org.junit.ComparisonFailure: 
Expected :1L
Actual   :0L

<p>I jest to rozsądne ponieważ najpierw przypisaliśmy wartość do <samp>v1</samp> równą <samp>v0</samp>, a potem podbiliśmy <samp>v0</samp>. Czyli wiemy już jak działa postinkrementacja w kotlinie (jak działa w <a href="http://www.zielonasowa.pl/przygody-tappiego-z-szepczacego-lasu-cz-1-7135.html">Szepczącym Lesie</a>? oto jest pytanie). Zmieńmy nasz kod na preinkrementację:</p>
<p class="listing">Listing 8. Preinkrementacja</p>kotlin
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);
    }
}

<p>Odpalamy i otrzymujemy</p>
<p class="listing">Listing 9. Wyniki testu preinkrementacji</p>bash
java.lang.AssertionError: expected not same:&ltVersion(version=1)&gt
<p>I to też jest zrozumiałe ponieważ najpierw podbiliśmy <samp>v0</samp>, a następnie przypisaliśmy wynik do <samp>v1</samp>. Czyli preinkrementacja działa też w rozsądny sposób. Zatem nasz kod testu w wersji finalnej będzie wyglądał tak</p>
<p class="listing">Listing 10. Działajkacy test</p>kotlin
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);
    }
}

<p>Możemy tu zaobserwować ważną rzecz. Operator <samp>++</samp> (oraz <samp>--</samp>) dotyka tylko zmiennej na rzecz której został wywołany. Obiekt reprezentowany przez  zmienną nie podlega podmianie chyba, że funkcja <samp>inc</samp> (albo <samp>dec</samp>) 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. </p>
<p>W przypadku operatora <samp>+a</samp> (i innych) możemy zwrócić cokolwiek. Reszta w <a href="http://kotlinlang.org/docs/reference/operator-overloading.html">dokumentacji</a>.</p>
<h4>Podsumowanie</h4>
<p>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 <samp>::</samp> albo <samp></samp> 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  to "dziwne" nazwy metod). </p>