Inversion to let keyword


#1

Is there any inversion to let keyword that would execute block of code only if a variable is null?

Let’s say that it would look like this:

savedInstanceState?!let {
    // execute code here
}

and it would be equivalent to this:

if(savedInstanceState == null) {
    // do something here
}

Maybe it might not be a good idea (I’m not the one to judge that), but as I like let keyword, I was thinking about an alternative.


#2

How about this:

savedInstanceState ?: run {
    // execute code here
}

And of course, you could always write your own extension function…
But I don’t believe that this will make your code easier to read.


#3

Thank you. I’ll give it a try and see if it’s more comfortable for me :slight_smile: I actually wanted to hear if it could be considered as a good practice or not. I’d understand both opinions.


#4

Personally I don’t consider safe navigation or elvis operator as replacement or regular if, perhaps in this specific use case.

Generally I use run and let when I need to catch the result, ie:

val instanceState = savedInstanceState ?: run {
    // compute new state here
}

#5

My opinion, based on examining the set of available and non-available permutations of functions like filter(NotXXX), map(NotXXX), let,run,map,use etc are intended to support the Convention which is nearly a direct language feature – of a particular style of chained invocations. It took me a long time to understand this as the particular style is fairly foreign to Java, quite common in Groovy and some other languages which I know less.

In Java, chained expressions (“wither”) are of the form
obj.method1().method2().publicField1.method3…
or for ‘builder’ or 'wither’
builder.withA(a).withB(b).withC© …

the only implicit ‘behind the scene’ behaviour is that each preceding expression, when evaluated, becomes the “this” implicit object for the next expression. ( null’s lead to NPE, void leads to ‘Doesnt compile’)

Enhanced in Java 8 with streams so one gets really nasty expressions like
collection.stream().map( new MyMappingFunctor() ).filter(…).flatMap().collect( Collectors.toList() ) .forEach( { … } … )
Same implict behaviour wrt ‘this’ --> for streams ‘this’ is a Stream type except for terminal functions.

Lambda’s add some simplification potential to the syntax and changes to the scope of the lamba (but not the subsequent expression. Lambdas (or any value) as a Argument to method have no consistent Symantec affect on the chaining, although many unique cases where the argument becomes the result value instead of passing along ‘this’ or a ‘this.withSomeModification()’.
The convention is also severely restricted by islands of type hierarchies. For example the Stream functions all require Stream (or derived) objects as ‘this’ and/or result. Similar with Collections, Iterators etc.
Learning one type hierarchy’s convention does little to help use another’s – and when it does, be careful, similarities may be coincidental not intentional.

Kotlin promotes a slightly different convention by implicit "it’, trailing lambda/closure simplified syntax, augmented scoping rules for nested classes and lambdas/closures and a set of library calls that look like native syntax, simplified function declaration syntax, auto-type infrequence, reworked generics, extension functions, delegates, scoping differences from Java, reified generic types, strongly consistent set of library functions that work cross-types, control flow statements as expressions (if/when/try…) infix notation, etc . Individually all interesting but rarely singularly compelling features. Together they enable but do not enforce a set of stylistic conventions based on both Java-like changing and 2 more dimensions – ‘this’ (‘target’) as distinct from scope, nullable, and ‘it’,

A off-the-cuff simplified example (and probably off ) of what you can see implemented and in heavy use in the stdlib source

Consider an object method class O { fun method( arg : T, block : (V) -> R ) -> W ) : X

For nearly every combination of equality, difference, existence, absence of O,T,V,R,W,X there exists a ‘syntax like’ expressions for chaining that maps O,T,V,R,W into O’,T’,V’,R’,W’,X’ in the called function - as a language syntax/keyword, library implementation, convention, or combination of the above.

Really 2: a terminal and non-terminal variant. ( ‘terminal’ being when X is Unit, you cant chain off the end of Unit, you CAN chain off the end of null )
e.g. Given

def a : String? = aString?ProducingExpression.

the expression (and most varients composed of fewer or empty inputs)
a.method1( a1 ) { this.method2( it ) }

where method1() returns type C
and method2() returns type D

can be used in a chaining expression where the scope of ‘this.method2(it) -> T’ is any mapping of the

current scope, ‘this’, ‘a’, ‘a1’ , C
onto
scope’ , this’ , it , D

a 4x4 sparse matrix
Add null-ability as an orthogonal dimension or a variant of each.

When used in isolation – functions like ‘let’ , ‘run’ etc have no great value over ‘if( expr ) … else’.
When used in a changing expression – very different – it takes some handstands to use ‘if’ as a step in the middle of a chaining (can be done, is ugly, IMHO).
Memorizing these all, particularly when to use which - derived from what the mapping from this,arg,scope,result is for each is very powerful – and something I have yet to achieve short of a few cheat sheets. Knowing it exists is a first step.

Dont want all that complexity ?
So “just dont chain” – sure. chaining is oft abused.

However Kotlin’s ‘val’ type is very useful, and requires an expression to initialize it - so you need a direct expression or a named function invocation or pre-existing object. (more code, more bugs).

Similar, the short version of function declarations (named or anonymous) and lambdas require a single expression. (or refer to a named function/object)

This implies that a (possibly chained) expression representation of a statement can be a significant, non-linear, benefit when used in combination. (avoiding labels, nested return’s , block expressions, named functions/variables etc).

The way these all fit together, yet simultaneously stand alone, is very elegant and compelling.
Like all shiny things, tempting and oft abused.
Like all subtle elegance, oft unrecognized and underutilized.
Art.
Like Art, the millions of choice-points one has to inject the decision of which method to use is largely arbitrary but simultaneously essential to the overall experience.

Note:
Recursively, all components of expressions must be expressions. Your ‘Main’ function could be composed of a single, possibly unimaginably long, expression.

main(args: Array) = a.very.long.chained.expression…

Lets not go there :slight_smile: Some Art is simply Ugly by ALL observers.

NOTE:
Google for ‘kotlin let use run’ … will find many good blogs including matrix’s and diagrams of how the permutations are represented, what maps to what in each, etc.