Let vs. with vs. run

I'm trying to understand why `Standard.kt` provides three differently named "global" functions (`let`, `with`, `T.run`) which appear to be extremely similar, and whose subtle differences I find difficult to tell from their names. In particular, what would be lost by replacing these methods with the single method below?

// allows to use implicit or explicit receiver based on circumstances (e.g. nesting) fun <T, R> T.with(f: T.(T) -> R): R = this.f(this)

If there is a good reason to keep all three functions, please explain the reasoning in the code docs.

A related question: What does run(f) give over f() or f.invoke()? (First time I came across this function, I thought it might perhaps run its argument in a separate thread.)

Thanks,
Peter

1 Like

There is an excellent review from Cédric Beust about these functions: Exploring the Kotlin standard library, highly recommend reading.

What does `run(f)` give over `f()` or `f.invoke()`

When you have an instance f of a functional type there is not much of use from run, but with functional literal run reads better. Compare for example:

val x = run {   .... // body }

and

val x = {   .... // body }()

Regarding to the question about is there a good reason to keep all three functions we have the following consideration: when such so-called scope function is used with a functional literal it introduces an identifier into the scope of the literal — either this or it, thus it may hide an identifier with the same name from the containing scope.
Despite there are ways to disambiguate hidden identifier, we’d like to provide some flexibility, so that one can choose which form is more convenient in particular situation.

In particular, what would be lost by replacing these methods with the single method below?

fun <T, R> T.with(f: T.(T) -> R): R

First, it hides both this and it in the scope of literal, which may be undesireable. Second, it requires a functional type with two parameters: first is the extension receiver T., second is an extension parameter (T). It's not a big deal for functional literals, but an instance of functional type (T) -> R, which is a function of one parameter, becomes incompatible and therefore could not be passed to such scope function.

1 Like

Reading that post actually led to my questions.

Update: I’m regularly using let (along with the even more useful apply), in particular x?.let { somehowTransform(x) }. I haven’t had any use for with, T.run or run. I still find the multitude of very similar global methods confusing, and would love to see KDoc explaining the rationale behind each of them.

5 Likes

I have made a blog to share my analysis of these functions. Hopes this helps. Mastering Kotlin standard functions: run, with, let, also and apply | by Elye | Mobile App Development Publication | Medium

2 Likes

See also this spreadsheet:

1 Like