How I use Kotlin Scope Functions to write better code

How I use Kotlin Scope Functions to write better code

I write a mix of Java, Kotlin, Ruby and Typescript throughout my day. Of those, I've always found Ruby to be the most eloquent. I clearly remember my first few weeks of writing Ruby. I wrote and re-wrote the same little program trying to find a style that clicked with me. During that process I stumbled across Enumerable mixin. I was hooked.

The Enumerable mixin is pretty simple on the surface. It adds a whole suite of helpful little methods to any collection class. For a collection classes think: set, list, hash. By leaning on the Enumerable mixin we're able to write code like this:

[ 3.1415, 2.7182, 1.6180 ]
	.reject { |number| number < 2 }
	.map { |number| number + 1 }
	.reduce(0) { |sum, number| sum + number }
# 7.8597

Okay, but what does this have to do with Kotlin? Ruby's Enumerable mixin is great for working with items in a collection, but Ruby doesn't really offer anything for chaining functions to the overall object itself (more on yield_self in another post).

Enter Kotlin. Kotlin has this concept of extension functions. You can write them like this:

fun String.isFizz() = this == "fizz";

fun main() {
  println("fizz".isFizz())
	// ^^^ That prints 'true', of course.
}

Which, when written in Java, would look something like this:

static boolean isFizz(string) {
	return string == "fizz";
}

public static void main(String[] args) {
	println(isFizz("fizz"));
}

Now that's pretty sweet but Kotlin actually comes with a few of these already built right in; available on any object. They're known as the Kotlin Scope Functions.

From Kotlin's docs on scope functions:

💡
The Kotlin standard library contains several functions whose sole purpose is to execute a block of code within the context of an object. When you call such a function on an object with a lambda expression provided, it forms a temporary scope. In this scope, you can access the object without its name. Such functions are called scope functions. There are five of them: let, run, with, apply, and also.

Scope Functions

NameContext Object ReferenceReturn ValueIs extension function?
let
it
lambda result
run
this
lambda result
when
this
lambda result
apply
this
this
also
it
it

let

.let is fantastic for replacing an object in a call chain. In the following example, we replace the YAML string with the deserialised MyClass object. We avoid the need to create an intermediary myYAMLObject before deserialisation.

val myObject: MyClass = """
		---
		characters:
			- Tom
			- Dick
			- Harry
	"""
		.trimIndent()
		.let { objectMapper.readValue(it, MyClass::class.java) }

run

Run works a lot like let. It replaces the object in the call chain. But expressions inside the run object work in the context of the object itself. Think of it as you calling methods from within the object.

val sizeOfList = mutableListOf("Tom", "Dick")
	.run { 
		append("Harry")
		size 
	}

// sizeOfList = 3

with

with works a lot like run except that it does not have a return value — so you can't use it in the context of a call chain.

with(listOf("Tom", "Dick", "Harry")) {
  println("The last element is ${last()}")
  println("There are $size elements")
}

// prints:
// The last element is Harry
// There are 3 elements

apply

.apply is wonderful for configuring objects, especially those without builders. In the following example, we're configuring an ObjectMapper in Jackson. We can successively chain .also statements allowing us to call methods in the context of the object mapper. With these chained .also expressions we can:

  • Register the Kotlin Jackson module
  • Allow JSON field names to be unquoted
  • Ensure that extra field names don't cause deserialisation failures
  • And configure the naming strategy to be kebab-cased.
val objectMapper = ObjectMapper(YAMLFactory())
  .apply { registerModule(KotlinModule()) }
  .apply { configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) }
  .apply { configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) }
  .apply { propertyNamingStrategy = PropertyNamingStrategy.KEBAB_CASE }

Writing this in Java we'd have to do something like this:

ObjectMapper objectMapper = ObjectMapper(YAMLFactory());
objectMapper.registerModule(KotlinModule())
objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
objectMapper.propertyNamingStrategy = PropertyNamingStrategy.KEBAB_CASE

Which might not seem so bad but imagine if we wanted it as a class-level variable.

class YAMLMapper {
	private static final OBJECT_MAPPER = ObjectMapper(YAMLFactory())

	public static YAMLMapper() {
		objectMapper.registerModule(KotlinModule())
		objectMapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true)
		objectMapper.propertyNamingStrategy = PropertyNamingStrategy.KEBAB_CASE
	}
}

// 🤢

With the Kotlin example, we've managed to keep the initialisation and configuration of the object together.

also

.also is great for intercepting a call chain and adding some action. I use it extensively to add logging, especially before returning from a method.

In the following example, I use .also to chain some logging calls to the end of an assignment call. That way, my object is still assigned to the right value (also always returns this) and any additional information I want gets logged out.

val myObject = Files.readAllLines(someFilePath)
    .joinToString("\n")
    .let { mapper.readValue(it, MyClass::class.java) }
    .also { LOGGER.info("Successfully read the configuration file.") }
    .also { LOGGER.info("Path: $someFilePath") }

How do you pick which scope function to use?

image

Personally, that means that I end up using let and also most of the time and apply only when I'm trying to replicate the builder pattern.