With,Use,Apply,Also,Run,Let

The two that do not fit in the same group are the Python With and Kotlin Use. These functions are for using (or doing something with) a resource that should be released when the operation is complete.

Kotlin Let, Apply, Run, Also, With (not to be confused with Python With), are all quite similar. Once you understand one, you understand all of them. Each allows operations on an object, without continually repeating the name of the object. Why so many choices? There are slightly different use cases, and they are explained here.

  • Python With/Kotlin Use
  • Kotlin
    • With
    • it vs this blocks
    • Result vs Object blocks
    • Apply
    • Also
    • Run
    • Let
  • kotlin ‘null’ if/else alternative

Python With/Kotlin Use

The with/use statement is for working with resources that may need setup and close operations. The main feature of the with/use is that the close operation will still be called even if an exception is raised during the code block. This ensures resources are closed or released, with or without an exception.

The classic use in Python is:

with open("sample.txt","w") as file:
    file.write("here is the text")

In python, ‘with’ statement requires an object that has both an ‘__enter__()’ and an ‘__exit__()’ method. In the above example, the call to ‘open’ returns a File object, which can be saved in the variable name following the ‘as’. File objects in python implement the required ‘__enter__()’ and ‘__exit__()’ methods, and are designed to be used in ‘with’ statements to ensure that the close() of the file will always take place, even if there is an exception, as the File.__exit__() method calls close.

Python programs can enable any resource class to work in with statements by simply defining enter and exit methods. The full details are explained in Python pep 343 and there are syntax examples just under the heading ‘Generator Decorator’ on that page. Note: __exit__() takes 3 parameters.

The equivalent Kotlin code to the python example is:

File("sample.txt").use {
   writeText("here is the text")
}
// now with an 'enter block' (not needed in most cases)
File("sample.txt").apply{
   // enter code goes here
}.use{
   writeText("here is the text")
}

There is no ‘enter’ equivalent built into Kotlin ‘use’, as for many resources (such as file) no ‘enter’ is required, but ‘enter’ code can be created using an ‘apply’ block and will then operate as with Python.

Python requires objects for ‘with’ statements implement ‘__enter__’ and ‘__exit__’, and Kotlin requires objects for ‘use’ statements implement the ‘Closeable‘ interface.

The python ‘with’ is a statement, but Kotlin ‘use’ is a method for all objects, so has that extra flexibility of being able to be used anywhere in an expression. The result of ‘use’ is the object the use method was called on.

Kotlin Block Operations

With

The ‘with’ function is the original block operation, as seen in java, pascal and some other languages. In Koltin, extension functions make implementations of methods (let, apply, also, run) a better choice as they allow for null safety and ‘with’ is really only a legacy feature for those familiar with ‘with’, and ‘run’ effectively replaces ‘with’, and adds the option of null safety and a better fit with the idiom of Kotlin.

The basic concept of ‘with’ is still applicable to all the Kotlin block functions so the explanation is still worth understanding. Consider this code before and after a ‘with’:

val shape:Shape()
// assume some code has used shape
//…..
val child = shape.children.first()
child.size *=2 // scale up
child.shade += 1 // darken shape
child.x +=2 // move to the right
child.reDraw()

// now same change using with
with(shape.children.first()){
  size *=2 // scale up
  shade += 1 // darken shape
  x +=2 // move to the right
  reDraw()
}

The version using ‘with’ is less cluttered because ‘shape’ does not have to be repeated over and over. When many operations take place with the same object, ‘with’ saves repeating the object name.

this vs it block operations

The ‘with’ function in the example above allows removing references to ‘shape’ as if the code of the block is part of the class of ‘Shape’. However, consider when the shape object has to be passed to functions. For example if reDraw is not a method of shape, but a function that takes a shape parameter, then the code change would be as follows (refactored to use ‘apply’ the method equivalent to ‘with’:

val child = shape.children.first()
moveRight(child ,2)   // function to move now - like child.x +=2 
reDraw(child)

// now using 'apply'
shape.children.first().apply {
  moveRight(this, 2)
  reDraw(this)
}

Changing reDraw(child) to reDraw(this) was not such a saving. Generally if there are lots of references to ‘this’, the object is being looked at from the outside and does not feel like ‘this’. For those cases when ‘it’ reads better than ‘this’, there is an alternate block method that takes the object as a parameter, allowing using this, or giving the parameter name as with any other block.

// now using 'also' - like'apply' but object is parameter
shape.children.first().also {
  moveRight(it, 2)
  reDraw(it)
}

If a ‘with’ operation (apply or run) ends up with ‘this’ many times within the enclosed block, then consider the corresponding ‘it’ operation (also or let). Conversely, if an ‘it’ operation ends up with ‘it.<property>’ (dot notation after the ‘it’) many times, maybe it will read better as a ‘with’ operation.

Object vs Result block operations

This vs it changes only how the code reads, but ‘result’ vs ‘object’ changes functionality by changing what is returned. All previous examples have not used the return value from the block, but both ‘apply’ and ‘also’ return the object, but they have equivalents, ‘run’ and ‘let’ that return the value calculated by the block. Here is an example of ‘apply’ vs ‘run’ where each use the result.

// first using 'apply'
val firstChild = shape.children.first().apply {
  x +=2 // move to the right
  reDraw(this)
}
//now using 'run'
val onScreen = shape.children.first().run {
  x +=2 // move to the right
  reDraw(this) // reDraw returns if the object was on screen
}

While both manipulate ‘firstChild’, the statement creating a very different variable for each case.

Apply (‘this’, returns ‘this’)

The best way to think of ‘apply’ is obtaining a reference to an object, but allowing the object to have changes applied to the object itself before the reference is stored. For a new object, this might be setup beyond what is performed by the constructor. The end result is ‘get an object, with null safety apply code to that object, then store the object or otherwise further process the object.

val firstChild = shape.children.first().apply {
  x +=2 // move to the right
  reDraw(this)
}  // retrieve the 'first child', apply changes, then save

Also (‘it’, returns it)

Also provides the exact same functionality apply, just using ‘it’ in place of ‘this,’ and therefore thinking of the object as ‘it’. Conceptually, running code that uses an object as a resource to do things external to an object, rather than code that runs in the context of the object and directly uses the properties of the object. Using the above terminology, the best way to think of ‘also’ is obtaining a reference to an object, and also allowing a block of code to do other things that make use of the object, all with null safety. The end result is ‘get an object’, also do other things that use that object, then store the object or otherwise further process the object.

val firstChild = shape.children.first().also {
  moveRight(it, 2)
  reDraw(it)
}  // retrieve the 'first child', also move it and rederaw it, then save it

// radically different example
fun fibonacci(n: Int) {
    var (x, y) = Pair(0, 1)
    (0..n).forEach{
        println("x $x")
        x = y.also { y += x } // simultaneous assignment x, y = y, y+x
    }
}

Run (‘this’, returns last expression)

The best way to think of ‘run’ is obtaining a reference to an object, so that calculations with that object can calculate a value, and allowing for null safety.

val onScreen = shape.children.first().run {
  x +=2 // move to the right
  reDraw(this) // reDraw returns if the object was on screen
} // get first child and run calculations with it to see if on screen

Let (‘it’, returns last expression)

The best way to think of ‘let’ is obtaining a reference to an object, so that calculations using that object can calculate a value, and allowing for null safety.

val onScreen = shape.children.first().let {
  moveRight(it, 2)
  reDraw(it) // reDraw returns if the object was on screen
}  // retrieve the 'first child', run the code block using it, the record the result

Kotlin ‘Null’ if/else alternative

If is common to see kotlin code that uses ‘foo?.let’ as an alternative to ‘if(foo!=null)’. The ?. operator in conjunction with null values becomes an alternative to Boolean logic.

There is a replacement for ‘if(foo!=null)’ what about ‘if(foo==null). The Elvis operator can provide for this case, which is effectively the ‘else’ case for the original.

foo?.let{
  // code conditional on foo != null
  // let is the choice if the result of the code is required
  // an advantage over 'if' is that 'it' is 'foo' but not nullable
}

foo ?: TODO() // simple one expression for foo == null

foo ?: run{
  // multiple statement for foo == null.... 
  // but perhaps if(foo==null) might be clearer
  // no 'not nullable' variable advantage over 'if'
}

foo?.also{
  // alternative to let
 // still replaces 'if' with benefit of not nullable 'foo'
 // also should be used when an 'else' is required, as 
 // as result of block will be null...not last expression 
 // using ?: to check for 'else' may give strange results with 'let'
} ?: run {
  //the else block...but could give strange results 
}

There we have all the syntaxes covered. The main advantage of the ?. as an alternative to ‘if’ that the non-nullable value is available within the block. Kotlin can sometimes use rules to determine that ‘foo’ would not be null inside the ‘if’ block, however, when ‘foo’ is a var that could be technically be changed in other threads, the ‘it’ parameter which is ‘foo when it is was known to be not null’ is a more certain non-null value.

Summary.

An alternative explanation can be found here. This summary could be expanded.

apply and also perform the same function, just swapping ‘this’ is swapped for ‘it’ within the code. Which of ‘apply’ or ‘also’ is more readable depends on the task. Generally, use ‘apply’ to configure an object, ‘also’ to use the object, but not the properties of the object.

Run and let are the ‘apply’ and ‘also’ equivalents – except each of these returns a value calculated by the block, rather than simply returning the original object. In fact ‘run’ and ‘let’ could be used in place of ‘apply’ and ‘also’, by ensuring the code block returns ‘this’ for ‘run’ or ‘it’ for ‘let’. The advantage of apply or also when reading code is that the reader knows the return value without even looking at the block.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s