🚀 · Closing Remarks & Exercises ⚙️

5 min read · Updated on by

Read on to learn how scope functions solve readability problems, not coding problems, how their usage is not standardized, how to use them properly, and a whole range of exercises to really get them down.

I hope this excursion into some of the most important functions from the Kotlin standard library has opened a new door in the way you think about designing code. One of the things that makes Kotlin unique is that it contains features solely for the benefit of the future reader, and not actual functionality. They are not there to make solving some type of problem easier, but to better express ourselves and communicate with the person who will be reading our code. Nowhere is this more true than in the context of scope functions.

However, as was said before, the meaning associated to these functions does not seem to be standardized. I have shown you my point of view, and shown you why I think it’s a reasonable one, but this is by no means what you will always encounter in the real world, and you don’t have to go far to find usages that are different. Indeed, the lead designer of Kotlin wrote an article in which he used with in a situation where, according to what was just said, he should have used apply or run. That does not make it right or wrong, merely open to interpretation. This is why I encourage you to find a style that you feel is right, and be consistent about it.

At the same time, I would like to stress that it is very easy to overuse scope functions, and in fact, many people do, especially when they first start out using Kotlin. While this is to be expected, it is good to keep in mind that usually, the only good reason to use a scope function is to communicate intent and/or make things easier to read. It is almost never a good idea to use scope functions only for the purpose of making code shorter, although, naturally, there are exceptions to every rule. As always, I encourage you to think critically about what you write, as opposed to binding yourself to a set of rules dogmatically.

I also strongly recommend watching the excellent talk Putting down the golden hammer, which discussed the abuse of Kotlin features (not only scope functions) in depth.

Exercises


import org.junit.Assert
import org.junit.Test

class Test {
    @Test
    fun testCocaine() {
        Assert.assertTrue(
            "Error when implementing weird cocaine things",
            doWeirdCocaineThings(null) == doWeirdThingsCalmly(null)
        )
    }
}

//sampleStart
class Company {
    lateinit var name: String
    lateinit var founder: String
    var objective: String? = null
}

/**
 * Ken came home from a cocaine party at 5am and decided it would be the
 * perfect opportunity to experiment with scope functions.
 *
 * Clean up his mess, using scope functions where sensible, and make the
 * output calmer.
 */

fun doWeirdCocaineThings(
    maybeCompany: Company?, 
    defaultName: String = "Abc", 
    defaultFounder: String = "That Dude"
) {
    (maybeCompany?.apply {
        println("Company Name! : ")
        print(name)
    } ?: Company().apply {
        println("Company is null!!!!! Creating, fast, yes...")
        name = defaultName
        founder = defaultFounder
        println("Company Name! AGAIN: $name")
    }).run {
        objective?.let {
            println("The objective!!! of company $name is $it")
        } ?: println("The company $name has NO OBJECTIVE!")
    }
}

fun doWeirdThingsCalmly(
    maybeCompany: Company?, 
    defaultName: String = "Abc", 
    defaultFounder: String = "That Dude"
) {
    TODO()
}
//sampleEnd

Solution


import org.junit.Assert
import org.junit.Test

class Test {
    @Test
    fun testFun() {
        var i = 0
        null.alsoUsingApply { i++ }
        null.also { i++ }

        Assert.assertTrue(
            "Error when implementing alsoUsingApply",
            i == 2
        )

        (fun Nothing?.() { i++ }) applyTo null

        Assert.assertTrue(
            "Error when implementing applyTo",
            i == 3
        )
    }
}

//sampleStart
/**
 * Implement 'also' using 'apply
 */

inline fun <T> T.alsoUsingApply(block: (T) -> Unit): Nothing = TODO()

/**
 * Implement a "reversed apply", that would allow writing e.g. Int::inc applyTo 5
 */
infix fun <T> (T.() -> Unit).applyTo(receiver: T): T = TODO()
//sampleEnd

Solution


import org.junit.Assert
import org.junit.Test

class Test {
    @Test
    fun testFunctionalList() {

        listOf(1, 2, 3).destructure { (head, tail) ->
            Assert.assertTrue("Error when implementing destructure - head is wrong", head == 1)
            Assert.assertTrue("Error when implementing destructure - tail is wrong", tail == listOf(2, 3))
        }

    }
}

//sampleStart
/**
 * A few chapters ago, we discussed a FunctionalList that would allow
 * us to destructure lists into a head and tail. We proceeded to define
 * a 'destructure' function that allowed us to take advantage of
 * destructuring done in a lambdas parameter list.
 *
 * Implement 'destructure' using 'let'.
 */

operator fun <T> List<T>.component1(): T? = firstOrNull()
operator fun <T> List<T>.component2(): List<T> = drop(1)

fun <T, R> List<T>.destructure(block: (List<T>) -> R): Nothing = TODO()
//sampleEnd

Solution


import kotlin.random.Random
import org.junit.Assert
import org.junit.Test

class Test {
    
    @Test
    fun testRandomString() {
        Assert.assertTrue(
            "Error when implementing nextString",
            Random.nextString(3, listOf('a')) == "aaa"
        )
    }
    
    @Test
    fun testRandomJson() {
        randomShallowJsonObject(3).let {
             Assert.assertTrue("Error when implementing randomShallowJsonObject - type doesn't match", it is JsonObject)
             Assert.assertTrue("Error when implementing randomShallowJsonObject - number of properties doesn't match", it.map.entries.size < 3)   
        }
    }
    
    @Test
    fun randomShallowJsonList() {
        randomShallowJsonList(3).let {
             Assert.assertTrue("Error when implementing randomShallowJsonList - type doesn't match", it is JsonList)
             Assert.assertTrue("Error when implementing randomShallowJsonList - number of properties doesn't match", it.size < 3)   
        }
    }
}

//sampleStart
/**
 * Rewrite the nextString extension function as an expression, 
 * i.e. fun Random.nextString(...) = <your code>
 */
const val DEFAULT_STRING_LENGTH = 5
fun Random.nextString(
    size: Int = DEFAULT_STRING_LENGTH, 
    alphabet: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')
): String {
    val stringBuilder = StringBuilder(size)
    repeat(size) {
		stringBuilder.append(alphabet.random(this))
    }
    return stringBuilder.toString()
}

/**
 * Type hierarchy representing an immutable JSON
 */
sealed class JsonElement

sealed class JsonScalar<T>(val value: T) : JsonElement()
class JsonNumber(value: Number) : JsonScalar<Number>(value)
class JsonString(value: String) : JsonScalar<String>(value)
class JsonBoolean(value: Boolean) : JsonScalar<Boolean>(value)
object JsonNull : JsonScalar<Nothing?>(null)

class JsonList(val list: List<JsonElement>) : 
	JsonElement(), 
	List<JsonElement> by list
class JsonObject(val map: Map<String, JsonElement>) : 
	JsonElement(), 
	Map<String, JsonElement> by map

fun randomJsonScalar() = when((1..4).random()) {
    1 -> JsonNumber(Random.nextDouble())
    2 -> JsonString(Random.nextString())
    3 -> JsonBoolean(Random.nextBoolean())
    else -> JsonNull
}

/**
 * Rewrite randomShallowJsonObject as an expression,
 * i.e. fun randomShallowJsonObject(maxAttributes: Int) = <your code>
 */
fun randomShallowJsonObject(maxAttributes: Int): JsonObject {
    val map = mutableMapOf<String, JsonElement>()
    repeat(Random.nextInt(maxAttributes)) {
		map[Random.nextString()] = randomJsonScalar()
    }
    return JsonObject(map)
}

/**
 * Rewrite randomShallowJsonList as an expression,
 * i.e. fun randomShallowJsonList(maxLength: Int) = <your code>
 */
fun randomShallowJsonList(maxLength: Int): JsonList {
    val list = List(Random.nextInt(maxLength)) {
		randomJsonScalar()
    }
    return JsonList(list)
}
//sampleEnd

Solution


import org.junit.Assert
import org.junit.Test

class Test {
    
    @Test
    fun testParityCount() {
        listOf(1, 2, 3, 4).countByParity().let { (evenCount, oddCount) ->
            Assert.assertTrue(
                "Error when implementing countByParity",
                evenCount == 2 && oddCount == 2
            )
        }
    }
}

//sampleStart
data class ParityCount(val evenCount: Int, val oddCount: Int)
/**
 * The following extension function counts the number of even and
 * odd integers in a list. Rewrite it using 'let'. Take advantage
 * of destructuring for bonus points.
 */
fun List<Int>.countByParity(): ParityCount {
	val partitioned = partition { it % 2 == 0 }
	return ParityCount(partitioned.first.count(), partitioned.second.count())
}
//sampleEnd

Solution

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer