Monday 3 June 2024

Kotlin Scope Functions

Scope function examples below:

  • let
  • with
  • run
  • apply
  • also
  • takeIf and takeUnless
  • Using them all at the same time!!!

I really hope I'm not the only one that gets confused about the different Scope functions1 in Kotlin and what they mean and when to use what.

The reference in [1] is excellent, but if I have to look up documentation on what certain methods do, the methods are not very well named.

So, in short, here's some examples, actually gleaned from the documentation and given my own spin on it with things that actually make sense.

Let

    /**
     * Useful if you do not wish to assign the result to an intermediate variable.
     * Useful with ? in case the result is NULL.
     */
    fun getDescription(address: Address?): String? =
        address?.let { address.getDescription() }

With

    /**
     * "with this object, do the following."
     * We're not interested in the result.
     */
    @Test
    fun withTest() {
        with(addressInEngland) {
            assertThat(street).isEqualTo("Morsestreet")
            assertThat(city).isEqualTo("London")
        }
    }

    /**
     * "with a helper object, do the following."
     */
    @Test
    fun withHelperTest() {
        val description = with(addressHelper) {
            computeAddress(addressInEngland)
        }
        assertThat(description).isEqualTo("12 Morsestreet London")
    }

Run

    /**
     * "run the code block with the object and compute the result."
     * Nice if you need to use it in an expression.
     */
    @Test
    fun runAsExtentionFunctionTest() {
        val didItWork: Boolean = database.run {
            val address = retrieveAddressFromDatabase()
            addressInAmerica.pobox = "43000"
            updateInDatabase(addressInAmerica)
        }
    }

    /**
     * "run the code block and compute the result."
     * Does not have a "this" or "it". Nice if you need to use it in an expression.
     */
    @Test
    fun runAsNonExtentionFunctionTest() {
        val didItWork = run {
            val address = database.retrieveAddressFromDatabase()
            with(shippingService) {
                sendItemToAddress(address)
            }
            log("Item sent")
            success
        }
    }

Apply

    /**
     * "apply the following assignments to the object."
     * The most common use case is object configuration, as below.
     */
    @Test
    fun applyTest() {
        val sameObject = Address().apply {
            housenumber = 12L
            street = "Morsestreet"
            city = "London"
        }
    }

Also

/**
     * "and also do the following with the object."
     * A common use case is to also assign the object to a property/variable.
     */
    private fun createHomeAddress() =
        Address().apply {
            housenumber = 12L
            street = "Morsestreet"
            city = "London"
        }
            .also { homeAddress = it }

TakeIf and TakeUnless

    /**
     * "return/use the value if this condition is true"
     * The opposite is takeUnless.
     */
    @Test
    fun takeIfTest() {
        assertThat(addressInEngland.takeIf { it.state != null }).isNull()
        assertThat(addressInAmerica.takeIf { it.state != null }).isEqualTo(addressInAmerica)
    }

And now for the big one!!!

    /**
     * Let's try all of them at the same time!
     */
    @Test
    fun allScopesTest() {
        val mailingSentForNewAddress = Address()
            .apply {
                housenumber = 12L
                street = "N. High Street"
                city = "Columbus"
                state = "Ohio"
                country = "United States of America"
            }
            .also {
                homeAddress = it
                with(database) {
                    updateInDatabase(it)
                }
            }
            .takeUnless { mailingAlreadySent(it) }
            ?.run {
                sendMailing()
                log("Mailing sent to ${getDescription()}.")
                success
            } ?: false

        val mailingSentForAddress =
            with(addressInEngland) {
                takeUnless { mailingAlreadySent(this) }
                    ?.run {
                        sendMailing()
                        log("Mailing sent to ${getDescription()}.")
                        success
                    } ?: false
            }
    }

Here I have tried to make use of the Scope functions in such a way that the different operations make sense.

P.S. I take offence on using "it" as the automatic name for the argument of the lambda. It sounds too much like the old Java "int i" in for loops. In other words, "it" has no meaning and the meaning depends entirely on context.

There's too much "it" and too much "this" and the context switching when using several Scope functions makes my head hurt.

References

[1] Kotlinlang.org - Scope Functions
https://kotlinlang.org/docs/scope-functions.html#functions