Scope Functions In Kotlin and When To Use Them

Scoping Things Out

Scope Functions In Kotlin and When To Use Them

Kotlin provides as part of its standard library a bunch of functions that allow us to execute a block of code within the context of an object. These functions expect a lambda expression to be provided and the content of that lambda is executed against the object in scope. There are five of these functions in total and they vary based on how you access the given object inside the block and what the result of the expression yields. These functions are named; also, apply, let, run and with. The functions themselves do not provide any additional functionality but aim to make code more concise, readable and functional in nature. One thing I particularly like about them is that when chained together they can make application code flow and read much like business requirements, use cases or whatever term you prefer.

                        
                            // Create the spec of a car to be built and persist in DB
// Build the car
// Register the car
// Ship the car
// Run the shipping job to include the car in the next shipment

with(Car(
	make: "BMW",
	model: "iX3",
	EngineTypes.ELECTRIC,
	EnvironmentalFriendlyRatings.GOOD
)) { this: Car
	createSpec(car: this).also { persist(car: this) }
	build(car: this)
	register(car: this)
	ship(car: this)
}.run { this: Unit
	invokeShippingJob()
}
                        
                    

In general scope functions are all quite similar, the subtleties arrive in the case of how and when best to use them. The main differences between each of these functions centre on the way we refer to the context object and the return value the scope function provides. Some scope functions identify the object reference as ‘it’, some as ‘this’ and in the case of the run function there is no object reference at all. Under the hood most of these scope functions are implemented via extension functions.

Looking at Java with the motive of identifying a counterpart is a fruitless exercise. There are no language features on the Java side that strongly compare to Kotlin’s scope functions. The only feature of Java that can be thought of in the same head space are probably anonymous classes. However, the similarities begin and end with the fact that like Kotlin’s scope functions, anonymous classes in Java are also expressions. The latter makes code harder to read in my opinion, requires more lines of code and isn’t flexible enough for you to chain the functional operations on these classes together without a bunch of annoying boiler plate (e.g., having to define functional interfaces).

Let’s look at each of the scope functions in turn and understand what they do and when is best to use them.

Also

The ‘also’ scope function allows us to refer to the context object as ‘it’ and provides as the return value the context object itself. The time to use the ‘also’ scope function is when we have a requirement for additional effects. For example, I often use ‘also’ when I create or update an object and thereafter wish to persist this in the database. I particularly like the fact that using it in this way makes the code read almost like plain English. As a rule of thumb, we should elect to use ‘also’ when we mainly care about the reference to that object and doing something with it, as opposed to any properties or functions on the object. With ‘also’ and indeed any scope function that refers to it’s context object as ‘it’ I always favour being explicit and replacing ‘it’ with a custom argument name. The reason being it attaches more meaning to the code and makes it easier to read. We should always strive to make our code as readable as possible anyway. However, this is even more crucial when we chain multiple scope functions together that all refer to ‘it’ inside their own internal scope.

                        
                            Car(
	make: "BMW",
	model: "iX3",
	EngineTypes.ELECTRIC,
	EnvironmentalFriendlyRatings.GOOD
)
	.also { car -> persist(car) }
                        
                    

Apply

The ‘apply’ scope function allows us to refer to the context object as ‘this’ and the return value in this case is the object itself. Apply is to all intents and purposes the same as the ‘also’ scope function, differing only in terms of how the context object is referenced. Its worth mentioning that for ‘apply’ and indeed any scope function where the context object is ‘this’ we can omit ‘this’ and refer to the context object’s properties implicitly. My general principle when using these types of scope functions is that I’m happy to omit ‘this’ in the event where the lambda is concerned with only the context object. However, in the scenario where we create other objects inside of the scope of the lambda I ensure I am explicit and use ‘this’ as I believe it makes the code easier to read. We should elect to use ‘apply’ for code blocks or function calls therein that don’t return a value. The typical use case for ‘apply’ is object configuration and in the case where we care about the object context’s properties and functions as opposed to the object itself.

                        
                            Car(
	make: "BMW",
	model: "x3"
).apply { this: Car
	engineType = EngineTypes.ELECTRIC
	environmentalFriendlyRating=EnvironmentalFriendlyRatings.GOOD
}
                        
                    

Let

The ‘let’ scope function is another one where the context object is available as ‘it’ and the return value is the lambda result. The time to use this scope function is when we are introducing an expression as a variable in local scope. A real-world example of when best to use the ‘let’ scope function is when we have a requirement to invoke one or more functions on the results of call chains. For example, we may want to create an object and then sort some property on that object and then print the results of that now sorted property.

                        
                            Car(
	make: "BMW",
	model: "iX3",
	EngineTypes.ELECTRIC,
	EnvironmentalFriendlyRatings.GOOD
	Listof("UK", "ESP", "USA", "POR", "AZB", "GER")
)
	.countriesWhereProduced
	.sorted()
	.let { country -> println(country) }
                        
                    

Run

The ‘run’ scope function does not provide any object reference and the result of using this scope function is the result of the lambda function provided. Unlike most other scope functions ‘run’ is not an extension function under the hood, which makes sense when you consider there is no context object in this case. You should use the ‘run’ scope function when after doing some body of work you have a requirement to run statements where an expression is required. A real-world example of this could be when after doing some body of work successfully you must notify something like a background job to kick off and you’d like visibility of whether this job ran successfully or not.

                        
                            when (determineProductionStatus()) {
	ProductionStatuses.READY ->
		produceBulkLotOfCars(make: "BMW", model: "IX3", country: "UK")
			.run { startPostProductionChecksJob()) }
			.apply { println("Status of post production checks were: $this") }
}
                        
                    

With

The ‘with’ scope function provides its object reference as ‘this’ and the return value is the result of the lambda provided. Like the ‘run’ function, ‘with’ is not an extension function and we provide the object context by passing it as an argument. It’s a good idea to use ‘with’ when we want to group function calls on an object. A real-world example of this may be when we have run a background job task and we want to log or print out various metrics of the execution of that background job task and to do that we must call multiple functions on that same object.

                        
                            with(retrievePostProductionChecksOutcome()){ this: PostProductionChecksOutcome
	println("The post production checks took $timeInMinutesToPerformChecks minutes")
	println("$numberOfChecksThatFailedToComplete checks failed to complete")
	printin("Checks were performed at the $plantName plant")
	println("Current production status is ${currentProductionstatus.name}")
}