
Most likely, from Java developers, and especially Android developers, many already know about
Kotlin . If not, it's never too late to find out. Especially if Java does not suit you with something like a language - which is probably true - or if you own Scala, but this language does not suit you, which is also not excluded.
In short, Kotlin is a statically typed language that focuses on JVM, Android (compiles into Java bytecode) and web (compiles into JavaScript). JetBrains, the developer of the language, set as its goal a concise and clear syntax, fast code compilation and type safety. The language is still in a pre-release state, but everything is rapidly moving towards release.
By the way, after Java, “retraining” to Kotlin is easy, this will help both understandable (subjectively) syntax and full compatibility with Java code in both directions, which allows the Java programmer to use the entire familiar set of libraries.
')
Another goal of language developers was the possibility of its flexible use, including for creating libraries that are similar in appearance to
DSL , and their own constructions (a good
example of a type-safe builder for HTML;
an article about the implementation of yield). Kotlin has several features that allow you to solve these problems efficiently and beautifully. Let's get to know them.
Extensions
In Kotlin, it is possible to complement the functionality of an arbitrary class, without inheriting from it, with functions (and properties) of the extension. The same possibility is, for example, in C #. It is worth noting that the behavior of extension functions differs from member functions: calls to extension functions are resolved statically, according to the declared type, and not virtually.
Example:
fun String.words(): List<String> { return this.split("\\W".toRegex()) }
toRegex (), drop (n), take (n) and joinToString ("") in the example are also extension functions.
Alternative syntax for function calls
1. An instance function or extension function that has only one argument can be called in infix form:
val squares = (1..100) map { i -> i * i } // (1..100).map({i -> i * i }) val multOfThree = squares filter { it % 3 == 0 } //it -
2. If the last argument of the function has a functional type, then when calling a function, you can write the corresponding lambda expression in parentheses, simply in curly ones:
val list = arrayListOf(1, 2, 3) with(list) { add(4) add(5) add(6) removeIf { it % 2 == 0 } } // with(list, { ... })
Inline functions
Kotlin has an
@inline
annotation that can be used to tag a function. After that, at compilation, the code of this function and its functional arguments will be substituted into the call sites. On the one hand, this gives some new opportunities (non-local return, reified generics), on the other hand, there is a restriction that the functional arguments of the inline-function in its body can only be called or passed to other inline-functions. The main effect of
@inline
is on performance: fewer function calls occur and, importantly, no anonymous classes and their objects are created for each lambda expression.
Most of the extension functions from the standard library like the same
map
and
filter
.
A small example:
@inline fun <T> Iterable<T>.withEach(action: T.() -> Unit) = forEach { it.action() }
Despite the fact that this code is replete with lambda expressions, no anonymous class will be created for them, and
i
will not even get into closure. Just a holiday!
Let's try?
Let's see what we can do with all this arsenal - suppose that we want to make such a
rather useless construction:
val a = someInt() val b = someList() val c = (a % b.size()) butIf (it < 0) { it + b.size() }
Unfortunately, this is not possible, but we will try to do something similar.
First attempt: function with two functional arguments
fun <T> T.butIf(condition: (T) -> Boolean, thenFunction: (T) -> T): T { if (condition(this)) { return thenFunction(this) } return this }
This is how it can be used:
val c = (a % b.size()).butIf({it < 0}) {it + b.size()}
If you add inline here, it should turn out quite effectively. Later we'll see how much, for now let's try to achieve a more beautiful syntax for this construct.
Second attempt: beautiful syntax
abstract class _ButIfPrefix<T> constructor(var originalValue: T) { abstract fun then(thenFunction: (T) -> T): T object trueBranch : _ButIfPrefix<Any?>(null) { override final inline fun then(thenFunction: (Any?) -> Any?) = thenFunction(originalValue) } object falseBranch : _ButIfPrefix<Any?>(null) { override final inline fun then(thenFunction: (Any?) -> Any?) = originalValue } } fun <T> T.butIf(condition: (T) -> Boolean): _ButIfPrefix<T> { val result = (if (condition(this)) _ButIfPrefix.trueBranch else _ButIfPrefix.falseBranch) as _ButIfPrefix<T> result.originalValue = this return result }
This option is not designed for multithreading! To use it in multiple threads, you will need to wrap the instances in ThreadLocal, which will further degrade performance.
There will be a chain of two infix calls, the first is the extension function on the object itself, the second is the function of the
_ButIfPrefix
instance. Usage example:
val c = (a % b.size()) butIf { it < 0 } then { it + b.size() }
Third attempt: currying
Let's try this:
fun <T> T.butIf0(condition: (T) -> Boolean): ((T) -> T) -> T { return inner@ { thenFunction -> return@inner if (condition(this)) thenFunction(this) else this } }
Using:
val c = (a % b.size()).butIf { it < 0 } ({ it + b.size() })
Compared with the first attempt, the arrangement of brackets in the call has changed. :)
Given the inline, we can expect that this option will work just like the first.
You can check this by looking at the bytecode: IntelliJ IDEA has a utility that shows the bytecode, which compiles the code on Kotlin, on the fly, and you can even see how the bytecode will be different with and without
@inline
.
Performance
Let's now see what will happen to the performance of our design in different versions.
We will test with this example:
val range = -20000000..20000000 val list = ArrayList<Int>() //warm-up for (i in range) { list add i % 2 } list.clear() val timeBefore = System.currentTimeMillis() for (i in range) { val z = (i % 2) butIf { it < 0 } then { it + 2 } // list add z } println("${System.currentTimeMillis() - timeBefore} ms")
Back we add to the comparison such code, which will be the benchmark of performance:
... val d = it % 2 val z = if (d < 0) d + 2 else d ...
According to the results of a fifty-four launch followed by averaging time, the following table was obtained:
Implementation | Without inline | C inline |
---|
Reference | 319 ms |
I try | 406 ms | 348 ms |
II attempt | 610 ms | 520 ms |
II attempt with ThreadLocal | 920 ms | 876 ms |
III attempt | 413 ms | 399 ms |
As you can see, the performance of the simpler first and third options is quite close to the standard, in some cases, the readability of the code can be “bought” for such an increase in operating time. The version with a more beautiful syntax is more complex and works accordingly longer, but if you want constructions quite similar to DSL, then it is quite applicable.
Total
Kotlin provides really flexible ways to “customize” a language, but sometimes you will have to pay for it with performance. The
@inline
can help improve the situation if there are first-order functions in your code. In any case, I think you will have good use cases for all this.
Good luck!