W poprzednich wpisach omówiliśmy sobie rozszerzenia i funkcje inline. Wiemy również, iż Kotlin ma bardzo przyjemnie zaimplementowane wyrażenia lambda. Czas połączyć te wszystkie rzeczy, dodać do nich jeszcze jedną, i zobaczyć co z tego wyjdzie…
Zapraszam do lektury.
Nie-zwykła Lambda
Naszym brakującym elementem jest tzw. „lambda z odbiorcą” (ang. „lambda with receiver”). Brzmi to groźnie, ale w rzeczywistości jest równie proste, co genialnie. Jest to po prostu możliwość wywołania lambdy na konkretnym obiekcie. Taki obiekt nazywamy wtedy „odbiorcą” (ang. receiver). Kiedy to zrobimy, wewnątrz takiej lambdy mamy bezpośredni dostęp do składowych tego obiektu.
Działa to bardzo podobnie do funkcji rozszerzających, w których wnętrzu mamy dostęp do metod rozszerzanego typu. Korzystając z analogii, można powiedzieć, iż lambdy z odbiorcą to takie „anonimowe funkcje rozszerzające”.
Notacja jest również podobna. Poniżej zamieściłem dwa typy funkcyjne. Pierwszy jest typową funkcją bezparametrową, zwracającą Unit. Drugi posiada dodatkowo zdefiniowany typ odbiorcy:
() -> Unit // zwykła funkcja String.() -> Unit // funkcja z odbiorcą typu StringWewnątrz takiej funkcji mamy bezpośredni dostęp do metod typu String, np.:
val printStats: String.() -> Unit = { println(length) println(toUpperCase()) println(toLongOrNull() ?: -1) println(startsWith("ABC")) } "DEF".printStats()Dość teorii. Zobaczmy jak to wygląda w praktyce.
Biblioteka standardowa
Podobnie jak to było w przypadku rozszerzeń, lambdy z odbiorcą również są często wykorzystywane przez samych twórców Kotlina – w bibliotece standardowej.
Przeanalizujmy jeden taki przykład – funkcję apply(). Jej definicja wygląda następująco:
public inline fun <T> T.apply(block: T.() -> Unit): T { this.block() return this }Co tu mamy? Jest to funkcja rozszerzająca dowolny typ T. Jej jedynym parametrem block jest funkcja. Jednak nie jest to zwykła funkcja, ale funkcja ze zdefiniowanym odbiorcą typu T.
Skutek jest taki, iż wewnątrz naszej funkcji rozszerzającej T.apply() możemy wywołać naszą lambdę block na instancji this, tak jakby była jego elementem składowym. Taka incepcja – rozszerzenie w rozszerzeniu
Ale co to nam tak naprawdę daje? Jak pisałem wcześniej, wewnątrz lambdy możemy teraz wołać metody typu T. Zobaczmy więc, jak wygląda wywołanie funkcji apply().
Poniżej typowy przykład z moich apek Androidowych:
recyclerView?.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) adapter = ProductListAdapter(dataItems, this@MyActivity) addItemDecoration(DividerItemDecoration(context, LinearLayoutManager.VERTICAL)) }Działa to tak, iż jeżeli recyclerView nie jest nullem, to wołamy kilka z jego metod i adekwatności. Zaletą jest fakt, iż żadnej z tych metod i adekwatności nie musimy poprzedzać przedrostkiem recyclerView..
Dodatkowo, fakt iż funkcja apply() jest inline sprawia, iż to rozwiązanie ma zerowy wpływ na wydajność. Po dekompilacji do Javy, będzie to wyglądać po prostu tak:
// Java if (recyclerView != null) { recyclerView.setHasFixedSize(true); recyclerView.setLayoutManager((LayoutManager)(new LinearLayoutManager(recyclerView.getContext()))); recyclerView.setAdapter((Adapter)(new ProductListAdapter((List)this.getDataItems(), (Context)this))); recyclerView.addItemDecoration((ItemDecoration)(new DividerItemDecoration(recyclerView.getContext(), 1))); }Kolejny przykład:
db.apply { execSQL(createTableSongSQL) execSQL(createTableAuthorSQL) execSQL(createTableLabelSQL) execSQL(loadFixturesSQL) }Takich przykładów można by mnożyć w nieskończoność. A ich głównym celem jest zwiększenie czytelności kodu po stronie wywołania.
Ukrywanie „ceremonii”
Ale to jeszcze nie wszystko. Zwróć uwagę, iż funkcja apply() nie robi nic prócz wywołania lambdy block. A przecież rozszerzenia to pełnoprawne funkcje, które mogą wykonywać dowolną logikę! I tutaj zaczyna się prawdziwa zabawa…
Posłużę się tutaj świetnym przykładem, jaki przedstawił kiedyś Jake Wharton w swojej prezentacji „Android Development with Kotlin”. Pamiętam, iż gdy zobaczyłem to rozwiązanie po raz pierwszy, zrobiło na mnie ogromne wrażenie. W tamtym momencie doszło do mnie, jak potężnym językiem jest Kotlin!
Załóżmy iż chcemy wykonać operacje na bazie danych, które wymagają transakcji. Standardowo w Androidzie taki kod wygląda następująco:
db.beginTransaction() try { db.delete("songs", "author = ?", arrayOf("Ed Sheeran")) // 1 db.setTransactionSuccessful() } finally { db.endTransaction() }Każdorazowo musimy tu dochować całej „ceremonii” – rozpocząć transakcję, stworzyć blok try-finally, zatwierdzić lub anulować transakcję… I gdzieś między tym wszystkim wykonujemy operacje na bazie, które wymagają transakcji (linia 1). Po pierwsze, ten kod jest nieczytelny. Po drugie, łatwo możemy zapomnieć np. o oznaczeniu transakcji jako „successful”. I żadna statyczna analiza kodu nam tego nie wykryje…
W Kotlinie, korzystając z kombinacji rozszerzeń, funkcji inline, i wyrażeń lambda z odbiorcą, możemy każdą taką „ceremonię” z łatwością zamknąć (ang. encapsulate) wewnątrz jakiejś funkcji.
Oto jak to wygląda w praktyce:
inline fun SQLiteDatabase.inTransaction(block: SQLiteDatabase.() -> Unit) { beginTransaction() try { block() // 1 setTransactionSuccessful() } finally { endTransaction() } }Stworzyliśmy tu rozszerzenie typu SQLiteDatabase. Jedyny parametr jest funkcją z odbiorcą, również typu SQLiteDatabase. Całość jest oznaczona jako inline, więc zawartość funkcji inTransaction() zostanie „wklejona” w każdym miejscu wywołania, po uprzednim „wklejeniu” zawartości lambdy block w miejscu wywołania tej lambdy (linia 1).
Jak to wygląda po stronie wywołania?
db.inTransaction { delete("songs", "author = ?", arrayOf("Ed Sheeran")) }Czyli udało się nam ukryć całą powtarzalną logikę (aka „ceremonię”) wewnątrz funkcji inTransaction(). Po stronie wywołania mamy tylko lambdę, w której umieszczamy jedynie te operacje na bazie, które powinny być zawarte w transakcji. Szczegóły implementacji transakcji są ukryte. Czyli idealnie
Anko Layouts
Oczywiście transakcje w bazie to tylko jedna z możliwości. Zastosowań tej strategii jest bez liku. Moim ulubionym przykładem jak dotąd jest DSL do tworzenia layout’ów Androidowych w bibliotece Anko. Oto mała próbka:
verticalLayout { val name = editText() button("Say Hello") { onClick { toast("Hello, ${name.text}!") } } }Wygląda trochę jak nie-Kotlin, prawda? Stąd właśnie modne ostatnio określenie DSL Ale, jak się bliżej przyjrzeć, to jest to najzwyklejszy Kotlin.
Najpierw mamy funkcję verticalLayout() z jednym parametrem – lambdą. Wewnątrz lambdy, kolejna funkcja – button() – tym razem dwuparametrowa: pierwszy typu String, drugi – lambda. Itd…
Kto choć raz próbował tworzyć layout dla Androida w kodzie, wie jaka to męczarnia. Natomiast tutaj mamy do bólu czytelny pseudo-kod, naśladujący strukturą XML’a.
Podsumowanie
Moim zdaniem, możliwości jakie daje kombinacja trzech elementów – rozszerzeń, funkcji inline, i wyrażeń lambda z odbiorcą – jest jednym z najlepszych przykładów geniuszu twórców Kotlina (brawo Andrey Breslav!). Wszystkie te elementy idealnie tutaj współgrają, dając nam developerom naprawdę niesamowite możliwości.
Jestem ciekaw, jakie Ty masz pomysły na zastosowanie tej wspaniałej trójcy? Na pewno nie raz natrafiłeś na API, które wymagało od Ciebie żmudnej „ceremonii”, a którą najchętniej byś „ukrył”, żeby jej więcej nie oglądać
To wszystko na dziś. Do następnego wpisu!