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(c) …
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
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.