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

The with/use statement is for working with resources that may need setup and teardown operations. The main feature of the with/use is that the teardown operation will still be called, even if an exception is raised during the code block. This ensures resources can be 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")

The format of the Python with is (from pep 343 and introduced in Python 2.5) :

with expression [as variable]:
    with-block

The expression must evaluate to an object with __enter__() and __exit__() methods. The optional as variable allows storing the result of the expression. Unlike other languages where the with-block is primarily to give direct access to an objects namespace, Python does not even have object name spaces. In Python, even within class definition, object attributes attributes are accessed using explicit ‘self’ object reference, so ‘with’ in Python implementing any such feature is not even possible.

In summary: Python ‘with’ means:

  • evaluate the expression to produce a result object
    • optionally link the result object to a name (as ‘name’)
  • call the __enter__() method for the expression result object
    • result.__enter()
  • execute the statements of the ‘with-block’
    • if an exception is raised in the block, ensure ‘exit’ is run before raising
  • call the __exit__() method for the expression result object
    • result.__exit__(exception_type, exception_value, traceback)

execute a block of statements, run the __enter__() method of an object before the statements and the __exit__() method at the end. The statements themselves are unchanged by being inside a Python ‘with‘ block. Kotlin ‘use‘ has __exit__(), but no __enter__(), and the statements inside the Kotlin ‘use‘ block are changed as they, unlike with Python, have access to the namespace of the ‘use‘ object.

The concept of the python ‘with’ is that object returned by the expression, is prepared prior to the with-block, by way of a call to the __enter__() method, and finalised at the end of the with-block by calling the __exit__() method of the object. In the example of the open("sample.txt", "w"), open returns a file object which had the required ‘enter’ and ‘exit’ methods, although as with many uses of with there is no need for the __enter__() which does nothing. The __exit__() method for a file closes the file and releases resources.

This call to the __exit__() to ensure the object is closed off, is the main use of Python with. Note that even if there is an exception within the with-block the exit() method will still be called, ensuring any resources are releases or other finalisation takes place.

File objects in python implement the required ‘__enter__()’ and ‘__exit__()’ methods to allow use with ‘with’ statements, in order 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{
   // any __enter__() code would go 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.

As the equivalent of Python objects used in ‘with’ statements implementing ‘__enter__’ and ‘__exit__’, 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, which provides extra flexibility of being able to be used anywhere in an expression. The value returned by ‘use’, is the object the use() method was called on.

Again, the key feature of with and use, is that the ‘exit()’ method will always be called, even when an exception occurs. Using these constructs ensures files will always be closed, or resources released, even when there is an exception.

Kotlin Block Operations

With (Kotlin, not Python 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. However the same result is often cleaner using ‘run’, and I would suggest using ‘run’ in place of ‘with’

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.

The advantage of this structure is greatest if the expression prior to the ?. will be used within the conditional code. That expression, and specifically now no longer nullable or optional, can be referenced by ‘it’ or ‘this’ depending on the use of also vs let, run, apply

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 result of expression will be result of run block 
}

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.

Advertisement

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 )

Facebook photo

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

Connecting to %s