Pisząc o Kotlinie, dość często w przykładach używam funkcji. Czym jednak są funkcje i co można z nimi zrobić? Dziś wprowadzenie, a w kolejnych wpisach funkcje inline i lambdy.

Funkcje, a metody oraz czym jest main

Jeżeli funkcja została zdefiniowana w ramach klasy:

Listing 1. Klasa z funkcją

class Example(){
   fun someFun(){
      //...
   }
}

to można ją traktować jako metodę. Ma ona oczywiście wszystkie własności funkcji. Jednak jest dowiązana do konkretnego obiektu i może wchodzić w interakcję z jego niepublicznymi składowymi. Jeżeli funkcję zdefiniujemy w obiekcie towarzyszącym, to sprawa ma się podobnie. Z tą różnicą, że będziemy myśleć o niej jak o metodzie statycznej.

Specjalne znaczenie ma funkcja main. Jest ona odpowiednikiem Javowego public static void main, czyli to od niej rozpoczyna się wykonanie programu.

Listing 2. Funkcja main

fun main(args: Array<String>) {

    println("Hello world!")

}

Funkcje zdefiniowane

Na listingu 2, użyliśmy funkcji println. Kotlin domyślnie dostarcza wielu funkcji, które należą do tych najczęściej używanych. Przykładowo, jeżeli chcemy utworzyć sekwencję, coś zrobić z każdym z elementów, a na koniec wypisać OK tyle razy ile było elementów, napiszemy:

Listing 3. Funkcje zdefiniowane

fun main(args: Array<String>) {

    sequenceOf(1, 2, 3, 4, 5, 6).map { TODO("Do something") }.forEach { e -> println("OK!") }

} 

Ciekawą funkcją jest TODO, która zwróci wyjątek NotImplementedError. Piękna sprawa. Szczególnie w połączeniu z TDD, gdzie jesteśmy w stanie śledzić czy testy padły, bo coś zostało źle napisane, czy też nie napisano tego w ogóle. Funkcja sequenceOf pozwala na zbudowanie sekwencji. Podobne funkcje są dostępne dla list, map, setów. Inną ciekawą funkcją jest synchronized, która zakłada blokadę na czas wykonania:

Listing 4. Przykład wykorzystania synchronized

fun main(args: Array<String>) {

    var value = 1;

    synchronized(value, {println(value++)} )

    println(value)

}

ctrl + spacja twoim przyjacielem.

Definiowanie funkcji

By zdefiniować funkcję, która nie jest składową klasy, w pliku piszemy:

Listing 5. Definiowanie funkcji

fun mojaFunkcja(){}

W tym przypadku funkcja nic nie robi. Zwraca jednak obiekt typu Unit. Typ ten ma tylko jedną wartość – Unit. Nie trzeba też podawać explicite, że będzie zwracany. Jeżeli już jesteśmy przy typie zwracanym, to warto pamiętać, iż musimy go podać:

Listing 6. Typ zwracany jest obowiązkowy (za wyjątkiem Unit)

fun mojaFunkcja(): String {
    return ""
}

Oczywiście funkcje mogą też przyjmować parametry. Kotlin dostarcza kilku przydatnych elementów w swojej składni, które pozwalają na wygodniejsze przekazywanie parametrów:

Listing 7. Parametry funkcji – wartości domyślne i parametry nazwane

fun main(args: Array<String>) {

    mojaFunkcja(toUpper = true, str = "make me up")
    mojaFunkcja("don't make me up")

}

fun mojaFunkcja(str:String, toUpper:Boolean = false): String {

    when{
        toUpper -> return str.toUpperCase();
        else -> return str;
    }
}

Parametry mają nazwy, więc w wywołaniu możemy je zamienić kolejnością (jeżeli ktoś tego potrzebuje). Drugi parametr, toUpper, ma podaną wartość domyślną, która zostanie wykorzystana, jeżeli parametru nie podamy. Jest to przydatne rozwiązanie, gdy chcemy np. stworzyć funkcję budującą konfigurację:

Listing 8. Wykorzystanie parametrów domyślnych

data class H2Connection(val username: String, val password: Array<String>, val host: String)

fun createConnection(username: String = "sa", password: Array<String> = arrayOf(""), host: String = "localhost",
                     port: String = ""): H2Connection {
    return H2Connection(username, password, "$host:$port")
}

fun main(args: Array<String>) {

    val con = createConnection(username = "me", host = "some.server")

    println(con)

}

Notacja infixowa

Na początku wspomniałem, że funkcje zdefiniowane w klasach można traktować jako metody. W szczególnych przypadkach można jednak użyć tzw. notacji infixowej w stosunku do tych funkcji (oraz w stosunku do rozszerzeń). Przykładowo:

Listing 8. Notacja infixowa dla metody

data class H2Connection(val username: String, val password: Array<String>, val host: String) {

    infix fun join(pool: Pool) {
       //... 
    }
}

//----------- albo wykorzystując rozszerzenia
infix fun H2Connection.join(pool: Pool) {
       //... 
}
//-----------

fun main(args: Array<String>) {

    val pool = Pool("extPool")
    val con = createConnection(username = "me", host = "some.server")

    con join pool;

}

Będzie poprawnym programem, a con join pool jest równoważnym zapisem con.join(pool). Jest to taki lukier składniowy, który pozwala budować proste „zdanka”.

Podsumowanie

Funkcje jak funkcje. Nie ma w nich nic ciekawego. Przynajmniej na podstawowym poziomie. Jednak są one przydatnym narzędziem, które może ułatwić tworzenie konstrukcji, które w Javie wymagały kombinowania.