📜 ⬆️ ⬇️

Kotlin puzzlers, Vol. 2: a new batch of puzzles



Can you predict how this Kotlin code will behave? Will it compile, what will it output and why?

No matter how good a programming language is, it can throw up such a thing that it remains only to scratch the back of the head. Kotlin is not an exception - there are also “puzzle players” in it, when even a very short code fragment has unexpected behavior.

Back in 2017, we published a selection of such puzzlers from Anton Cake antonkeks on Habré. And later he spoke with us on Mobius with the second selection, and now we also translated it into a text view for Habr, hiding the correct answers under the spoilers.

We also attach the video of the speech, if something is incomprehensible in the text, you can refer to it.


The first half of the puzzler is aimed at those who are not very familiar with Kotlin; the second half is for hardcore Kotlin developers. We will run everything on Kotlin 1.3, even with progressive mode enabled. The source code for the puzzler is on GitHub . Whoever has new ideas, send pull requests.

Pazzler №1


fun hello(): Boolean { println(print(″Hello″) == print(″World″) == return false) } hello() 

Before us is a simple hello function, it runs a few print. And we run this function itself. A simple question for overclocking: what should it print?

a) HelloWorld
b) HelloWorldfalse
c) HelloWorldtrue
d) will not compile

Correct answer


The first option was correct. The comparison is triggered after both print have already started, it cannot start earlier. Why does such code compile at all? Any function other than returning Nothing returns something. Since in Kotlin everything is an expression, even return is also an expression. The return type return is Nothing, it is cast to any type, so you can compare it like this. And print returns Unit, so Unit can be compared with Nothing as many times as necessary, and everything works great.

Pazzler №2


 fun printInt(n: Int) { println(n) } printInt(-2_147_483_648.inc()) 

A hint so that you do not guess: a scary number is a really minimal possible 32-bit signed integer.

Here everything looks simple. Kotlin has excellent extension functions like .inc () for incrementing. We can call it on Int, and we can print the result. What happens?

a) -2147483647
b) -2147483649
c) 2147483647
d) None of the above

Run!


As can be seen from the error message, there is a problem with Long. But why Long?

The extension-functions have priority, and the compiler first starts inc (), and only then the minus operator. If inc () is removed, it will be int, and everything will work. But inc (), starting first, turns 2_147_483_648 into Long, because this number without a minus is no longer valid Int. It turns out Long, and only then minus is called. All this cannot be transferred to the printInt () function, because it requires an Int.

If we change the printInt call to regular print, which can accept Long, then the second option is correct.



We see that this is actually Long. Beware of this: not all puzzle players can run into the real code, but this one is possible.

Pazzler №3


 var x: UInt = 0u println(x--.toInt()) println(--x) 

In Kotlin 1.3 came new great features. In addition to the final version of Korutin, we
now finally we have unsigned numbers. This is necessary, especially if you are writing some kind of network code.

Now, even for literals there is a special letter u, we can define constants, we can decrement x, as in the example, and convert to Int. I remind you that we have a sign with Int.

What happens?

a) -1 4294967294
b) 0 4294967294
c) 0 -2
d) will not compile

4294967294 is the maximum 32-bit number that can be obtained.

Run!


The correct option is b.

Here, as in the previous version: first, toInt () is called on x, and only then decrement is called. The result of the decrement unsigned, and this is the maximum from unsignedInt.

The most interesting thing is that if you write like this, the code will not compile:

 println(x--.toInt()) println(--x.toInt()) 

And for me it is very strange that the first line is working, and the second is not, it is illogical.

And in the pre-release version, the correct version would be C, so well done in JetBrains, that fixes bugs before the release of the final version.

Pazzler №4


 val cells = arrayOf(arrayOf(1, 1, 1), arrayOf(0, 1, 1), arrayOf(1, 0, 1)) var neighbors = cells[0][0] + cells[0][1] + cells[0][2] + cells[1][0] + cells[1][2] + cells[2][0] + cells[2][1] + cells[2][2] print(neighbors) 

In this case, we ran into the real code. We at Codeborne did Coding Dojo, together we implemented them on the Kotlin Game of Life . As you can see, Kotlin is not very convenient to work with multi-level arrays.

In Game of Life, an important part of the algorithm is determining the number of neighbors for a cell. All the ones around are neighbors, and it depends on this whether the cell will live on or die. In this code, you can count edinichki and assume that it will.

a) 6
b) 3
c) 2
d) will not compile

let's get a look


The correct answer is 3.

The fact is that the plus from the first line is moved down, and Kotlin thinks that it is unaryPlus (). As a result, only the first three cells are added. If we want to write this code in several lines, we need to move plus to the top.

This is another one of the “bad puzzlers”. Remember, in Kotlin it is not necessary to transfer the operator to a new line, otherwise it may consider it unary.



I have not seen situations where unaryPlus is needed in real code, except DSL. This is a very strange topic.

This is the price we pay for the lack of semicolons. If they were, it would be clear when one expression ends and another begins. And without them, the compiler must decide itself. A line break for a compiler very often means that it makes sense to try to examine the lines separately.

But there is one very cool JavaScript language, in which you can also not write a semicolon, and this code will still work correctly.

Pazzler №5


 val x: Int? = 2 val y: Int = 3 val sum = x?:0 + y println(sum) 

This puzzler was delivered by KotlinConf speaker Thomas Nild.

Kotlin has a great nullable feature. We have a nullable x, and we can convert it, if it turns out to be null, through an Elvis operator to some normal value.

What will happen?

a) 3
b) 5
c) 2
d) 0

Run!


The problem is again in the order or priority of the operators. If we reformat it, the official format will do this:

 val sum = x ?: 0+y 

Already the format suggests that 0 + y starts first, and only then x?:. Therefore, naturally, 2 remains, because X is equal to two, it is not null.

Pazzler №6


 data class Recipe (var name: String? = null, var hops: List<Hops> = emptyList() ) data class Hops(var kind: String? = null, var atMinute: Int = 0, var grams: Int = 0) fun beer(build: Recipe.() -> Unit) = Recipe().apply(build) fun Recipe.hops(build: Hops.() -> Unit) { hops += Hops().apply(build) } val recipe = beer { name = ″Simple IPA″ hops { name = ″Cascade″ grams = 100 atMinute = 15 } } 

When I was called here, I was promised a craft beer. I'm going to look for him tonight, have not yet seen. Kotlin has a great theme - builders. In four lines of code, we write our DSL and then create it through the builders.

First, we create IPA, add hops called Cascade, 100 grams in the 15th minute of cooking, and then print this recipe. What have we got?

a) Recipe (name = Simple IPA, hops = [Hops (name = Cascade, atMinute = 15, grams = 100)])
b) IllegalArgumentException
c) will not compile
d) None of the above

Run!


We got something similar to craft beer, but there’s no hop in it, it’s gone. They wanted IPA, and got the "Baltic 7".

Here is the naming clash. The field in Hops is actually called kind, and in the line name = ″ Cascade ″ we use name, which is trapped with the name of the recipe.

We can create our own BeerLang annotation and register it as part of the BeerLang DSL. Now we are trying to run this code, and we should not compile it.



Now we are told that, in principle, name cannot be used from this context. DSLMarker is for this purpose necessary, that inside the builder the compiler did not allow us to use the external field, if we have the same inside, so that there is no naming clash. The code is corrected like this, and we get our recipe.



Pazzler №7



 fun f(x: Boolean) { when (x) { x == true -> println(″$x TRUE″) x == false -> println(″$x FALSE″) } } f(true) f(false) 

This puzzler was one of JetBrains employees. Kotlin has a feature when. It is for all occasions, allows you to write cool code, often used together with sealed-classes for API design.

In this case, we have a f () function that accepts a Boolean and prints something depending on true and false.

What will happen?

a) true TRUE; false false
b) true TRUE; false true
c) true FALSE; false false
d) None of the above

let's get a look


Why is that? First, we evaluate the expression x == true: for example, in the first case it will be true == true, which means true. And then there is also a comparison with the sample that we passed to when.

And when x is set to false, calculating x == true will give us false, but in the sample it will also be false - so the example will match the sample.

There are two ways to fix this code. One is to remove x == in both cases:

 fun f(x: Boolean) { when (x) { true -> println(″$x TRUE″) false -> println(″$x FALSE″) } } f(true) f(false) 

The second option is to remove (x) after when. When works with any conditions, and then will not compare with the sample.

 fun f(x: Boolean) { when { x == true -> println(″$x TRUE″) x == false -> println(″$x FALSE″) } } f(true) f(false) 


Pazzler №8


 abstract class NullSafeLang { abstract val name: String val logo = name[0].toUpperCase() } class Kotlin : NullSafeLang() { override val name = ″Kotlin″ } print(Kotlin().logo) 

Kotlin was sold as a “null safe” language. Imagine that we have an abstract class, it has some name, and also a property that returns the logo of this language: the first letter of the name, just in case made uppercase (all of a sudden, it was initially forgotten to be capitalized).

Since the language is null safe, we will change the name and probably should get a correct logo, which is one letter. What do we really get?

a) K
b) NullPointerException
c) IllegalStateException
d) will not compile

Run!


We received a NullPointerException that should not be received. The problem is that the superclass constructor is first called, the code tries to initialize the property logo and take the null char from the name, and at this point the name is null, so a NullPointerException occurs.

The best way to fix this is to do this:

 class Kotlin : NullSafeLang() { override val name get() = ″Kotlin″ } 

If we run such code, we get "K". Now the base class will call the base class constructor, it will actually call the getter name and get Kotlin.

Property is a great feature in Kotlin, but you need to be very careful when you make override properties, because it is very easy to forget, to make a mistake, or to do something wrong.


Puzzler number 9


 val result = mutableListOf<() -> Unit>() var i = 0 for (j in 1..3) { i++ result += { print(″$i, $j; ″) } } result.forEach { it() } 

There are mutableList of some terrible things. If it reminds you of Scala, then it is not in vain, because it really looks like. There is a List lambda, we take two counters - I and j, increment and then do something with them. What happens?

a) 1 1; 2 2; 3 3
b) 1 3; 2 3; 3 3
c) 3 1; 3 2; 3 3
d) none of the above

Let's run


We get 3 1; 3 2; 3 3. This happens because i is a variable and it will retain its value until the end of the function. And j is already passed by value.

If instead of var i = 0, val i = 0, this would not work, but then we could not increment the variable.

Here in Kotlin we use a closure, this feature is not in Java. It is very cool, but it can bite us if we do not immediately use the value of i, but pass it to the lambda, which starts later and sees the last value of this variable. And j is passed by value, because the variables in the loop condition are like val, they don’t change their value anymore.

In JavaScript, the response would be “3 3; 3 3; 3 3 "because nothing is passed by value.


Pazzler number 10


 fun foo(a:Boolean, b: Boolean) = print(″$a, $b″) val a = 1 val b = 2 val c = 3 val d = 4 foo(c < a, b > d) 

We have a function foo (), takes two Boolean, prints them, everything seems simple. And we have a bunch of numbers, left to see which figure is greater than the other, and decide which option is correct.

a) true, true
b) false, false
c) null, null
d) will not compile

Run


Not compiled.

The problem is that the compiler thinks it looks like generic parameters: with <a, b>. Although it seems like “c” is not a class, it is not clear why it should have generic parameters.

If the code were like this, it would work fine:

 foo(c > a, b > d) 

It seems to me that this is a bug in the compiler. But when I come to Andrei Breslav with any such puzzler, he says “this is because the parser didn’t want it to be too slow”. In general, he always finds an explanation why.

Unfortunately, this is so. He said that they will not fix it, because the parser is
Kotlin still does not know about semantics. First there is a parsing, and then it further transfers to another component of the compiler. Unfortunately, this will probably remain so. So do not write two such angle brackets and any code in the middle!

Puzzler number 11


 data class Container(val name: String, private val items: List<Int>) : List<Int> by items val (name, items) = Container(″Kotlin″, listOf(1, 2, 3)) println(″Hello $name, $items″) 

Delegate is a great feature in Kotlin. By the way, Andrei Breslav says that this is the feature that he would like to remove from the language, he does not like it anymore. Now, perhaps we will find out why! And he also said that companion objects are ugly.

But the data classes are beautiful. We have a data class Container, it takes its name and items. And at the same time in the Container we implement the type of items, this is a List, and we delegate all its methods to items.

Then we use another cool feature - destructure. We "destruct" the elements of the name and items from the Container and display them on the screen. It seems that everything is simple and clear. What happens?

a) Hello Kotlin, [1, 2, 3]
b) Hello Kotlin, 1
c) Hello 1, 2
d) Hello Kotlin, 2

Run


The most incomprehensible option is d. It turns out to be true. As it turned out, elements simply disappear from the items collection, and not from the beginning or from the end, but remain only in the middle. Why?

The problem of destructuring is that due to the delegation all the collections in Kotlin also
have their own destructuring option. I can write val (I, j) = listOf (1, 2), and I get these 1 and 2 into variables, that is, the List has implemented functions component1 () and
component2 ().

The data class also has component1 () and component2 (). But since the second component in this case is private, the one that is public from the List wins, so the second element is taken from the List, and here we get 2. The moral is very simple: don't do that, do not do that.

Puzzler number 12


The following puzzle is very scary. This is caused by a person who is somehow connected to Kotlin, so he knows what he is writing.

 fun <T> Any?.asGeneric() = this as? T 42.asGeneric<Nothing>()!!!! val a = if (true) 87 println(a) 

We have an extension function on nullable Any, that is, it can be applied to anything at all. This is a very useful feature. If it is not already in your project, you should add it, because it can cast everything on you, on everything, on anything. Then we take 42 and cast it in Nothing.

Well, if we want to be sure that we have done something important, you can instead !!! write !!!!, the Kotlin compiler allows you to do this: if you lack two exclamation points, write at least twenty-six.

Then we do if (true), and then I myself do not understand anything ... Let's immediately choose what happens.

a) 87
b) Kotlin.Unit
c) ClassCastException
d) will not compile

We look


It is very difficult to give a logical explanation. Most likely, the Unit turns out here due to the fact that there is nothing more to push. This is an invalid code, but it works because we used Nothing. We cast something to Nothing, and this is a special type that tells the compiler that an instance of this type should never appear. The compiler knows that if the possibility of Nothing appears, which is impossible by definition, then you can not check further, this is an impossible situation.

Most likely, this is a bug in the compiler, the JetBrains team even said that maybe this bug will someday be fixed, this is not a very priority. The point is that we have deceived the compiler here because of this caste. If you remove the line 42.asGeneric <Nothing> () !!! and stop cheating, the code will stop compiling. And if we leave, the compiler goes crazy, thinks this is an impossible expression, and stuffs everything into it.

I understand that. Maybe someone will explain it better.


Puzzler number 13


We have a very interesting feature. You can use dependency injection, and you can do without it, make singletones through object and run your program cool. Why do you need Koin, Dagger or something? Test, however, will be difficult.

 open class A(val x: Any?) { override fun toString() = javaClass.simpleName } object B : A(C) object C : A(B) println(Bx) println(Cx) 

We have a class A open for inheritance, it takes something inward, we create two objects, a singleton, B and C, both are inherited from A and pass each other there. That is a great cycle is formed. Then we print what B and C got.

a) null; null
b) C; null
c) ExceptionInInitializerError
d) will not compile

Run


The correct option is C; null

One would think that when the first object is initialized, there is no second one yet. But when we print this out, C doesn’t have enough B. That’s, the order is reversed: for some reason, the compiler decided to initialize C first, and then it initialized B together with C. It looks illogical, it would be logical, on the contrary, null ; B.

But the compiler tried to do something, he did not succeed, he left there null and decided we did not throw anything. This can also be.

If Any? in the type of parameter to remove?, it will not work.



You can say bravo to the compiler for when null was resolved, he tried, but failed, and if? No, he throws an exception to us that we cannot do a cycle.

Puzzler number 14


In version 1.3 there were excellent new Korutin in Kotlin. I thought for a long time how to come up with a puzzle about corutin so that someone could understand it. I think for some people, any code with corortines is a puzzler.

In 1.3, some of the function names changed, which were in 1.2 in the experimental API. For example, buildSequence () is renamed to just sequence (). That is, we can make excellent sequences with the yield function, infinite loops, and then we can try to get something out of this sequence.

 package coroutines.yieldNoOne val x = sequence { var n = 0 while (true) yield(n++) } println(x.take(3)) 

With the Korutins, it was said that all the cool primitives that exist in other languages, such as yield, can be done as library functions, because yield is a suspend function that can be interrupted.

What will happen?

a) [1, 2, 3]
b) [0, 1, 2]
c) Infinite loop
d) None of the above

Run!


The correct option is the last one.

Sequence is a lazy thing, and when we cling to it, it is also lazy. But if you add toList, then it would really be deduced [0, 1, 2].

The correct answer is not related to Corutin at all. Korutiny really work, they are easy to use. For the sequence and yield functions, it is not even necessary to connect a library with corortines, everything is already in the standard library.

Puzzler number 15


This puzzler is also a developer from JetBrains. There is such a hell of code:

 val whatAmI = {->}.fun Function<*>.(){}() println(whatAmI) 

When I saw him for the first time, during KotlinConf, I could not sleep, trying to understand what it was. Such a cryptic code can be written on Kotlin, so if someone thought that Scalaz is scary, then Kotlin is also possible.

Let's guess:

a) Kotlin.Unit
b) Kotlin.Nothing
c) will not compile
d) None of the above

Let's run


We got Unit, which came from no one knows where.

Why? First, we assign the variable lambda: {->} - this is a valid code, you can write an empty lambda. It has no parameters, it returns nothing. Accordingly, it returns Unit.

We assign a lambda to a variable and immediately write an extension to this lambda, and then we launch it. In fact, she will simply reserve Kotlin.Unit.

Then on this lambda you can write an extension function:

 .fun Function<*>(){} 

She is declared on the Function <*> type, and what we have on top also suits her. Actually, this is Function <Unit>, but I didn’t write to Unit so it would be more incomprehensible. Know how an asterisk works in Kotlin? This is not the same as a question in Java. Она выбирает тот тип, который лучше всего подходит.

В итоге запускаем эту функцию, и она возвращает Unit из {}, потому что она ничего не возвращает, это void-функция. Непонятно, зачем так писать, но можно. Анонимная функция-расширение, которую пишешь и сразу вызываешь — такое тоже бывает.

This completes the puzzlemakers. In conclusion, I want to say that Kotlin is a cool language. If you are an iOS developer and saw it for the first time today, then what you see does not mean that you don’t need to write to Kotlin!
Если вам понравился этот доклад с Mobius, обратите внимание: следующий Mobius состоится 22-23 мая в Петербурге . Там без Kotlin тоже не обойдётся — доклад «Coroutining Android Apps» поможет не наломать дров при переходе к корутинам. Будет и много другого для мобильных разработчиков (как Android, так и iOS), уже известные подробности о программе — на сайте , и с 1 марта стоимость билетов повысится.

Небольшой лайфхак: если не хотите покупать билет, у вас еще есть шанс попасть на конференцию в качестве спикера — до 6 марта мы принимаем заявки на доклады.

Source: https://habr.com/ru/post/440974/