Světýlkův blog

Hlášky, myšlenky, Kotlin a kiting ...
  • Má lenost se nedá měnit, ale pouze kultivovat.

  • Implicitně slušivý oblek netřeba explicitně chválit.

  • Signály těla? Asi to chtěla …

  • Hra Hangman (Šibenice) v Kotlinu

    Rád s nadsázkou říkávám, že Kotlin je tak produktivní, že občas už ani sám nevím, co bych v něm dál napsal, neboť jak by se na Ostravsku řeklo „bo všecko je už zrobene“ 🙂 . A tak jsem si při příležitosti víkendové nudy řekl, že si jen tak napíšu jednoduchou konzolovou hru Hangman (Šibenice/Oběšenec). Pokud byste dokázali kód refaktorovat do ještě lepší podoby, podělte se v komentářích 🙂 , nicméně tady to je a přeji příjemnou zábavu:

    package com.svetylkovo.games.hangman
    
    object Hangman {
    
        private var remainingWrongAttempts = 5
        private var word = ""
    
        private val guessedLetters = mutableSetOf<Char>()
        private val wrongGuesses = mutableSetOf<Char>()
    
        @JvmStatic
        fun main(args: Array<String>) {
    
            println("Let's play Hangman!")
            println("Type a word to be guessed:")
    
            word = readLine()?.nullIfBlank() ?: "HANGMAN"
    
            clearScreen()
    
            println("\nStart guessing!\n")
    
            while (remainingWrongAttempts > 0) {
                val hiddenWord = getHiddenWord()
    
                if ('_' !in hiddenWord) {
                    println("You did it!")
                    return
                }
    
                println("Word: ${hiddenWord.replace(Regex("."), "$0 ")}")
                printWrongGuesses()
                println("Remaining wrong attempts: $remainingWrongAttempts")
                println("Guess a letter:")
    
                val letter = readLine()?.nullIfBlank()
                    ?.toUpperCase()
                    ?.first()
    
                if (letter != null) {
                    guessedLetters += letter
    
                    if (letter in word) {
                        println("\nCorrect!\n")
                    } else {
                        println("\nWrong!\n")
                        wrongGuesses += letter
                        remainingWrongAttempts--
                    }
                }
            }
    
            println("You've been hanged! :-(")
        }
    
        private fun printWrongGuesses() {
            if (wrongGuesses.isNotEmpty()) {
                println("Wrong guesses: ${wrongGuesses.joinToString(", ")}")
            }
        }
    
        private fun getHiddenWord() = word.map { letter ->
            if (letter in guessedLetters) letter else '_'
        }.joinToString("")
    
        private fun clearScreen() = repeat(25) { println() }
    
        private fun String.nullIfBlank() = if (isBlank()) null else this
    }
    
  • Pokud mezi vámi nepřeskočila jiskra, musíte ještě trochu zakřesat.

  • Musíš aktivně neutrácet, abys mohl pasivně šetřit.

  • Jakožto nositel dioptrických brýlí zahajuji operaci Smítko.

  • Zábava je umění přesvědčit svůj mozek, aby vypustil nějaký ten endorfin.

  • Nepřišel, a tím přišel o hodně.

  • Kotlin a tail recursion

    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 🙂 .