Ještě za mých “dávných” dob Scaly jsem si vzpomněl na takovou funkcionální pikantnost, která by se vám možná mohla hodit. Říká se jí tail recursion (optimization) a vztahuje se pouze na ten nejjednodušší typ rekurze, kdy funkce volá čistě jen samu sebe. Typicky by člověk tohle využil u procházení nějakých zanořených stromových struktur, ale abychom trochu potrápili mozkové závity a třeba se i někam dále posunuli, zkusíme si něco úplně jiného. Představme si, že jsme v ryze funkcionálním světě a chceme iterovat přes nějaký List a to bez použití jakýchkoli cyklů. Tzn. žádný while, ani for, nic … Je něco takového vůbec možné? Ano, je, a jak jistě tušíte, dá se to řešit právě pomocí rekurze.
Challenge
Ještě než vám ukážu svou verzi, která vás konec konců svou jednoduchostí možná až překvapí, zkuste trochu potrápit své mozkové závity a pokuste se naimplementovat metodu recursiveForeach() pouze za pomocí rekurze tak, aby jí šlo použít takovýmto způsobem:
object TailRecursionTest { @JvmStatic fun main(args: Array) { val nums = listOf(1, 2, 3) recursiveForeach(nums) { println("The number is $it") } } }
Řešení
Jeden ze způsobů, jak to napsat, je např. tento:
fun <T> recursiveForeach(list: List<T>, callback: (T) -> Unit) { if (list.isNotEmpty()) { callback(list.first()) recursiveForeach(list.drop(1), callback) } }
a když daný kód spustím, na výstupu dostanu skutečně:
The number is 1
The number is 2
The number is 3
Limity stacku a klíčové slovo tailrec
Jak jistě čekáte, tohle řešení má docela podstatnou nevýhodu, a sice pokud bude list příliš velký, dostanete StackOverflowError. Zkuste si např v kódu změnit:
val nums = (1..10000).toList()
Co s tím? Stačí před fun recursiveForeach přidat kouzelné slůvko tailrec. Co tohle udělá, je optimalizace tohoto rekurzivního kódu pomocí cyklů, takže již nedojde k zanořování a následnému přetečení zásobníku. Pokud spustíte kód nyní, StackOverflowError již nenastane. Když se podíváme pod pokličku a dekompilujeme si výsledný kód, uvidíme něco takového:
public final void recursiveForeach(@NotNull List list, @NotNull Function1 callback) { while(true) { Intrinsics.checkParameterIsNotNull(list, "list"); Intrinsics.checkParameterIsNotNull(callback, "callback"); Collection var3 = (Collection)list; if (var3.isEmpty()) { return; } callback.invoke(CollectionsKt.first(list)); list = CollectionsKt.drop((Iterable)list, 1); } }
Sice rekurze až tak často nepíšu, ale někdy by se tato featura mohla určitě hodit 🙂 .