Kotlin, puzzlers and 2 Kekses: Are you sure you know how Kotlin behaves?
Initially, there was Java (well, not that at the very beginning ... but our story begins right here), time passed, and after about 20 years, the smart guys from JetBrains designed and released Kotlin, a “better” Java, universal language, understandable, powerful and transparent.
At one time, Andrei abreslav Breslav said that Kotlin was developed as a convenient and predictable language. At the same time the opinion was voiced that in this language you will not find puzzlers (short pieces of code, the results of which turn out to be unexpected, frightening or disappointing). Well, Anton antonkeks Keks conjured into IDEA and dug up some of it, moreover, he told about his findings together with Philip Keks in good examples. See for yourself:
')
Under the cut - a selection of such puzzlers and detailed comments to them. The material is based on the report by Anton Keks (Codeborne) and Philip Keks (Creative mobile) at the 2017 Mobius conference (St. Petersburg) . Let's start with Kotlin. Everyone says that there are a lot of problems in Java: there are a lot of volcanoes on the island, there are earthquakes. She needs to be saved.
Therefore, another island comes to mind - Kotlin.
There is calm, nothing happens. It is very flat, no volcanoes. Located here nearby. Therefore, Kotlin is a savior of Java, especially for Android developers like us.
A few words about Kotlin
What is Kotlin, here more or less everyone knows. Because what fool today writes under Android without Kotlin? This, I think, is masochism. It works great. A couple of weeks ago came the first build of Kotlin native. Soon, perhaps, we will write on Kotlin under iOS.
This is a pragmatic language, open-source, a very cool tooling - it was designed to make the IDE work well. This is a stone in the garden of Apple-ovsky language Swift and others like him. JetBrains is good push-it Kotlin - specially designed the language for your IDE.
We all know that Kotlin was developed for a very long time. Six years passed before version 1.0 was released. JetBrains tried very hard, but apparently it’s not so easy to make a new language. Over the past years (2010 - 2016), they even managed to change the logo to a more modern one.
Considering how long it was developed, the language must be excellent. It should be the best language in the world, as many other languages ​​have developed much faster. For example, we all know that JavaScript was made in two weeks. Although, this, of course, is not rocket science (rocket science is SpaceX, which in four years learned to sit on a platform on a real rocket).
And most importantly, you should all be proud, because Kotlin is being developed in St. Petersburg, and this is one of the few Russian programming languages. Old Russian programming languages ​​looked something like this (right):
Fortunately, this language was aimed at an international audience, so instead of the keyword “fu”, its creators still decided to use keyword fun, over which everything was “making fun”. So this is a fun language.
Jigsawners
What are jigsaw puzzles?
These are short programs written in Kotlin, with interesting behavior. And you will guess what they are typing. You will vote for the suggested answers, and the one who first raises his hand and not only guesses the correct option, but also explains why this happens, will receive a prize.
The first half of the gigs are targeted at those who are not very familiar with Kotlin; the second half is for hardcore Kotlin developers.
Kotlin is known for not repeating some well-known Java puzzle gamers. However, in an ideal programming language there should be no puzlers at all. It turns out that Kotlin is not perfect either - there are no perfect languages.
But Kotlin has already taken off in many mobile applications. This language was created as pragmatic and convenient in many cases. It is also convenient in terms of usability. And it is still developing. You can talk with Kotlin developers and try to agree on how and what to fix.
All of the demonstrated jigsawners run with Kotlin 1.1.1 - with the latest stable version. The source codes of the gushers are on GitHub - then you can see them: https://github.com/angryziber/kotlin-puzzlers/tree/mobius . Who will have the ideas of new puzlers, send pull-requests. We wait.
Pasler 1
Kotlin is good because he supports nullability, more precisely, he is null safe - so to speak.
package p1_nullean val s: String? = nullif (s?.isEmpty()) println("true")
It has differences between nullable and non-nullable types. This means that if we want to assign a null somewhere, it must be a nullable type (with a question). Probably, this idea was proposed by C #, but he did not finish it - there only primitives can be nullable. And in Kotlin, this is already done normally for all types. Basically, the language is designed so that you never get a terrible NullPointerException in runtime.
In this example, Kotlin took over from Groovy an excellent null-safe operator s ?, which allows you to call some method at zero and not to shake any exceptions right away in runtime.
Let's see which of the possible options we get now:
nothing
true
NullPointerException
Will not compile
We start. We look.
Not compiled.
Why?
Kotlin is a type safe language, so the result of the expression s? .IsEmpty () is null, so it does not cast to false.
It is easy to fix (it is necessary to write in such a way that Kotlin behaves in the same way as Groovy):
if (s?.isEmpty() ?: false) println("true")
It is not very beautiful, but now it is. In Kotlinovsky tracker, the proposal has nevertheless been taken to still interpret null as false, but there are some nuances there. It is not yet known whether we will ever get this feature or not. There are different opinions, but in terms of ease of use of the language is a small joint.
Pasler 2
Pazler is very similar: we have the same nullable string variable and we are trying to call the method on it.
package p2_nulleanExtended val x: String? = null print(x.isNullOrEmpty())
What will be the result?
true
false
NullPointerException
will not compile
Running ... Answer: true.
Why? This is an extension-function from the standard Kotlin library, which is “hung” on the nullable CharSequence. Therefore, in a similar case, it is processed normally.
Indeed, in Kotlin, you can run some functions on null. The compiler knows this and allows it.
If we put a question mark, IDEA would tell us that it is not needed here.
print(x?.isNullOrEmpty())
It is good that the function is named humanly (the result can be guessed by name).
If run in the second variant (with a question mark), the result will be null, because the expression in brackets is now given in null, and then this function is not called, despite the fact that it supports null. This, by the way, is another passer.
In Kotlin there is such an interesting feature - the third state of nullability. Let's see what happens if we call:
val prop = System.getProperty("key")
and pass it to the hello method of the Kotlin class, which should print it:
Kotlin().hello(prop)
What will be the output?
hello
hello null
will not compile
none of the above options
In general, type inference is a great topic.
We start. We get IllegalStateExeption.
Why?
The value of prop will be null, the type is String! .. It goes to hello, and in runtime there will be a check that it should not be null, but it should be null.
In fact, in the initial version of Kotlin they really did that when a String comes from Java, it is always nullable by default.
val prop: String? = System.getProperty("key")
This led to the fact that it became very inconvenient to write code when interop with Java was being implemented. And they decided to make a virtual type String! (with an exclamation mark). This is the third option nullability - called "I do not know."
val prop: String! = System.getProperty("key")
However, such code is not compiled because the type is String! can not be declared independently. It can only come from Java.
Therefore, it is better to pre-declare such things as nullable or not nullable (as a rule, you from the API know if null can ever come there or not).
But such code will compile, but may fall in runtime:
val prop: String = System.getProperty("key")
IDEA always knows where what type. You can click on the variable Ctrl + q and find out. But let's finish with nullability, let's move on to another topic.
Pasler 4
We have 2 functions that should print. We announce and launch them - everything should be simple:
Main1 will return a unit, but will call print ("Hello"). And main2 just returns the lambda, which will not be executed.
You can fix it like this:
main2()()
The second, in my opinion, the best fix is ​​to remove the equal sign from main2, because it only confuses everyone:
funmain2() { print("Hello 2") }
Why did I call this example Kotlin vs Scala? Those who wrote on Scala know that there this code is an absolutely valid declaration of a function that returns something:
funmain2() = { }
Poor Scala-developers who will write on Kotlin. They probably will constantly return lambda without running.
Pasler 5
We have a list of numbers, we iterate over it with the forEach method. ForEach, as in Groovy, if the lambda parameter is not declared, knows it. And we check that it is no more than 2, and we print.
In Kotlin, return returns from a function. And in order to get out of a specific lambda, inside this function you need to specify the name of the lambda after return:
if (it > 2) return@forEach
So you can get out of the lambda. If you are writing in Java and you really need to get out of lambda, this is an option to correct the code.
In fact, the return in Kotlin works the way it should work. If we hadn’t written in C # and Java before, we probably wouldn’t have made a mistake, because return is returned from the main function. Everything is logical. And there are no strange features with lambdas, from which, too, for some reason you need to exit.
Why does this work like this?
The forEach function is declared as an inline function. In Kotlin, the compiler does not call this function in the compiled code, but takes the code of this function and inserts it into the place where the call was. As a result, the usual for-loop is obtained here and, naturally, then return exits the main function.
How to understand that this is an inline function? First, in IDEA there is Ctrl + p. And secondly, if you call return, and the function is not inline, the compiler will say: "Sorry, you can not do this." That is, the compiler does not allow us to do some nonsense.
There is another option, how to fix this code so that it returns “12ok”. It is necessary to declare it as a function, not lambda.
The only difference in Kotlin with an anonymous function and lambda is that the first one behaves exactly like a function, which means that return will return from the nearest “fun” (fun). Therefore, with such a fix, it will work as it should.
To make it even more interesting, I prepared several examples. In Kotlin there are different keywords:
fun
inline fun
inline fun with lambda noinline
inline fun with lambda crossinline
Some of them allow you to use return, and some do not.
With homework we leave the wish to understand what Crossinline is. I think you will be interested.
When I first started writing on Kotlin, I also thought that it was something complicated. But when you understand what an inline function is (almost all the extension functions for the collection are Inline for performance), everything becomes very logical.
Pasler 6
We need to get John or Jaan.
We have a simple Person class. In Kotlin it is very convenient: you can declare the constructor immediately after the class declaration. We get the constructor variable name, hammer it into the property. In Kotlin, there is no field - there is only property, which is very cool, since you do not need to write getters, setters, and all nonsense (or gets and sets, as in C #). Great beautiful syntax.
As a result, we create a Person with the name John and see if it will turn into the Estonian localization of Jaan:
package p6_getMeJohn class Person(name: String) { var name = nameget() = if (name == "John") "Jaan" elsename } println(Person("John").name)
John
Jaan
will not compile
none of the options
Running ...
This is a stack overflow. Why? We take the name, make it an if-else and call it by get. To fix, you need to refer to the field, and not to the property. You can use a keyword field:
class Person(name: String) { var name = nameget() = if (field == "John") "Jaan" else field }
According to keyword field in Kotlin, you can refer to the field, but the only place where this can be done is inside the getter / setter. All other calls go only through property - they don’t apply directly to the field.
It is said that the performance is all cool, because the Java Hotspot compiler optimizes this well, unlike the .NET virtual machines, and everything works very fast.
Pasler 7
Again, we look at the awesome feature of the language - type inference - we don’t care what type of whatAmI we can use it anyway. But the compiler knows what it is. See if we know.
package p7_whatAmI val whatAmI = {}() println(whatAmI)
What option will be in the end?
kotlin.jvm.functions.Function0
() -> kotlin.Unit
kotlin.Unit
nothing
Run ... Get kotlin.Unit.
Why?
Here lambda is declared, then lambda is called. Since lambda does not return anything (more precisely, it returns kotlin.Unit), this is exactly what is output. And the best definition of a unit is void.
Where did Unit come from? In my opinion, even in mathematics (or in computer science) there is such a thing as type theory. And there it is described that Unit is one element, which means “nothing”. Therefore, some more academic programming languages ​​use the term Unit. Kotlin was zadizaynen as a pragmatic language, but, nevertheless, its developers decided to choose not a pragmatic void, but invented to make the Unit.
To make it even more interesting, there is one more type in Kotlin: kotlin.Nothing. What is the difference? Let the answer to this question be your homework.
Pasler 8
We looked at whatAmI, and now we will have iAmThis.
Everything is a little more complicated here: we have the IAm class, it is the data class (this is an awesome feature in Kotlin, which automatically generates equal, hashCode, toString for us, and all this boiler plate, which we all so hate to write in Java). In Scala, this is the case class - the name is worse for this, although in fact everyone uses it as a data class.
The class IAm has a constructor in which we declare the foo field. Foo is a property at the same time, so it can be used with the hello () function.
We pass the String “bar” there, call the hello function and see what it returns to us.
Apply is a clever extension-function. It takes lambda and allows inside it with the object on which it is called to perform some actions on this. Accordingly, this is bar. And Hello is bar.
This Kotlin is similar to JavaScript. As in JavaScript, in Kotlin you can reach the state where you no longer know what this is.
Generally there are many useful features: also, let, with.
In principle, they all differ quite a bit.
For example, apply is an extension-function for absolutely any type (not nullable). It takes lambda, and lambda is very tricky, because it is applied to the inner T, and not to the outer object (it has its T inside this lambda). Those. the function calls this lambda with its this and returns this (this is sometimes also useful).
There are other features. The code can be corrected as follows:
For some cases, Apply is a very useful feature. But if you look at the code very quickly (and at the same time the first variant of the record is used), you can get confused.
In the first variant, you can shorten the code like this (the apply function also returns this itself, so nothing changes):
Let's look at the let function already known to us.
This puzzle sent by Kevin Most from Canada. It has a simple function that prints the sign of the argument (Int).
package p9_weirdChaining // by Kevin Most @kevinmost fun printNumberSign(num; Int) { if (num < 0) { "negative" } elseif (num > 0) { "positive" } else { "zero" }.let { println(it) } } printNumberSign(-2) printNumberSign(0) printNumberSign(2)
What kind of code will print?
negative; zero; positive
negative; zero
negative; positive
zero; positive
We start ... At the exit - zero; positive.
What is the matter?
If is actually an expression. That is, two expressions are obtained, and let applies only to the second.
I wrote a lot on Kotlin, but this puzzle was not decided by myself. This is a hell of a topic. At the previous JPoint conference, we even thought it was a bug in the compiler. But I asked Andrei Breslav, and it turned out that this is just a parser nuance.
In this case, the top expression goes separately - the let function is not applied to it. There is no operator elseif in Kotlin (if it were, then this puzzleman wouldn't exist).
As with all puzzles, the moral is: don't write such code. If you want to do something complicated (like here), be sure to put brackets or put it in a variable and then call let.
Pasler 10
Even more interesting puzzler. There is a lot of code.
This passer Zababmittil Daniel Vodopyan. This is a very cool feature in Kotlin - delegate properties. In Kotlin, we can declare, for example, that there are several properties in the class, and they are implemented not as a field, but as a lukap from the map.
We have a class Population - population. And cities sends us (var cities: Map <String, Int>) and we delegate them to this map.
This actually allows you to turn Kotlin into JavaScript and make more dynamic structures without copying data back and forth. Such classes reduce a lot of code.
Then we create an instance of the class Population and pass it to all cities population.
Now imagine that many years have passed. People have dirtied the Earth - flew to live on Mars. Therefore, we reset the map with the population.
There is a function with, which we looked at before. It takes the population and resolves the available fields with respect to it (in principle, in the same way as apply).
package p10_mappedDelegates // by Daniil Vodopian @voddan class Population(var cities: Map<String, Int>) { val tallinn by cities val kronstadt by cities val st_petersburg by cities } val population = Population(mapOf( "st_petersburg" to5_281_579, "tallinn" to407_947, "kronstadt" to43_005 )) // Many years have passed, now all humans live on Mars population.cities = emptyMap() with(population) { println("$tallinn; $kronstadt; $st_petersburg") }
Everything is easy. It remains only to understand what will happen to our Earth when everyone flies to Mars. What does this code give out?
0; 0; 0
407947; 43005; 5281579
NullPointerException
NoSuchElementException
We are launching ... It turns out that humans have not disappeared anywhere (life on Mars is very difficult, so we will most likely stay on Earth).
Why?
It is not true to say that population.cities = emptyMap () will make an empty map for the class, but not for its instance. If we change the code like this (let's make MutableMap and reset Kronstadt - population.kronstadt = 0):
classPopulation(var cities: MutableMap<String, Int>) { val tallinn by cities var kronstadt by cities val st_petersburg by cities } val population = Population(mutablemapOf( "st_petersburg" to 5_281_579, "tallinn" to 407_947, "kronstadt" to 43_005 )) // Many years have passed, now all humans live on Mars population.kronstadt = 0
The code will display: 407947; 0; 5281579
But we are still discussing the first option (c population.cities = emptyMap ()).
When we execute the delegate, the reference to the map is remembered inside the getter (for each of them). And if we change the reference to cities, this does not change the links inside the getters. But we can even put another in the map in the map, and everything will work, since it still remains a link to the same map. But if we change the reference to another map, then it ceases to act.
Pasler 11
We in Estonia have an excellent saying: “A good child has many names.”
Let's see how this applies to our classes.
In Kotlin there is such a strange nuance: the default classes are final - they cannot be extended. There is a keyword open, which still allows them to extend.
In this C class puzzler, we have an open method (too, so that we can trust it). Here we take x and y (they have default values ​​- this is a very cool feature in the language).
We have class D, which extende class C and override the sum function, but in principle does not do anything useful, except for what causes super-implementation.
Next we have the variable d - we create an instance of class D; we have a variable c and there we assign the same instance (we get 2 references to the same instance of class D). And we call the same method essentially on the same object.
package p11_goodChildHasManyNames openclassC{ openfunsum(x: Int = 1, y: Int = 2): Int = x + y } classD : C() { overridefunsum(y: Int, x: Int): Int = super.sum(x, y) } val d: D = D() val c: C = d print(c.sum(x = 0)) print(d.sum(x = 0)) println()
What we get in the end?
22
eleven
21
will not compile
We start ... The correct answer is 21.
There are still some warning signs that help you understand what is happening.
In both cases, an over-defined function is called because of polymorphism. In runtime, which function is called, because in reality both c and d are instances of class D. But since the JVM does not have such features as nominal parameters, the compiler-time compiler solves them. Those. it turns out that the function is selected and called in runtime, and the parameters are selected in the compile-time. Therefore, what parameters it substitutes depends on the type of the variable, and not on the object resulting in runtime. This is a cant. Warning — and warn that your names should not be confused — when you override a function, it should be called differently.
The good news is that for about half of the submitted puzlers in IDEA, there is already a warning. Due to the fact that JetBrains themselves are also engaged in tools, they are quite good in helping to avoid many mistakes. But not all. For some of the puzzlers, warning is simply impossible to do.
However, the language develops. In 2016, when I first started writing on it, there were far fewer inspections at IDEA and it was much easier for these puzzles to get their hands on it. Now the situation is completely different: version 1.1 has been released, there have been many patch releases, many inspections have been added to IDEA, and it is now very easy to write on Kotlin correctly.
Instead of a conclusion I want to say: go to Kotlin.
Under Android, there is still no normal Java 8, and in Kotlin you get all the features of Java 8 and even more. You can express yourself much better.
Kotlin - a language without a big HYIP. This is also his plus.
It is often called “Swift” for Android. But there is a small problem with Swift - when a new version comes out, you have to constantly rewrite all the code. With Kotlin, there is no such problem - we are promised backward compatibility, as well as source-level, and binary-level.
Kotlin compiles much faster than Scala. It is much easier Scala.
It is much faster in runtime than Groovy. If you add your application on Android, the size in my opinion increases by only 600 KB compared to Java - and this is very small compared to Scala. Therefore, it makes sense to write on it.
When I switched to it, I started to be productive from day one.
They say about Kotlin that this is “better Groovy”, there are some good features.
And your most important friend in IDEA is Ctrl + Alt + Shift + K, which will convert any Java class directly into Kotlin (as is). There is no Ctrl + Alt + Shift + J, so you can not go back - this is the one way road. Yes, you do not want to come back.
Also passes gradle.
Send us new puzzles so that we can have fun at the next conferences.
If you like the programming interior just as we do, and you want to immerse yourself in Kotlin more thoroughly, we recommend paying attention to these reports that will be at the upcoming Mobius 2017 Moscow conference :