Хабрахабр

Kotlin puzzlers, Vol. 2: новая порция головоломок

Скомпилируется ли он, что выведет и почему? Можете предсказать, как поведёт себя такой Kotlin-код?

Kotlin не исключение — в нём тоже встречаются «паззлеры», когда даже у совсем короткого фрагмента кода оказывается неожиданное поведение. Как бы хорош ни был язык программирования, он может подкинуть такое, что останется только в затылке чесать.

А позже он выступил у нас на Mobius со второй подборкой, и её мы теперь тоже перевели для Хабра в текстовый вид, спрятав правильные ответы под спойлеры. Ещё в 2017-м мы публиковали на Хабре подборку таких паззлеров от Антона Кекса antonkeks.

Видеозапись выступления также прилагаем, если по тексту что-то окажется непонятным, можете обращаться и к ней.

Первая половина паззлеров ориентирована на тех, кто не очень хорошо знаком с Kotlin; вторая половина — для хардкорных Kotlin-разработчиков. Всё будем запускать на Kotlin 1.3, даже с включенным progressive mode. Исходные коды паззлеров находятся на GitHub. У кого появятся идеи новых, присылайте pull-реквесты.

Паззлер №1

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

Перед нами простенькая функция hello, она запускает несколько print. А мы запускаем саму эту функцию. Простой вопрос для разгона: что она должна напечатать?

a) HelloWorld
b) HelloWorldfalse
c) HelloWorldtrue
d) Не скомпилируется

Правильный ответ

Сравнение срабатывает после того, как оба print уже запустились, оно не может запуститься раньше. Первый вариант был правильным. Любая функция, кроме возвращающей Nothing, возвращает что-то. Почему такой код вообще компилируется? Возвращаемый тип return — это Nothing, он приводится к любому типу, поэтому можно так сравнивать. Так как в Kotlin всё — это выражения, то даже return — тоже выражение. А print возвращает Unit, поэтому Unit можно сравнивать с Nothing сколько угодно раз, и всё классно работает.

Паззлер №2

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

Подсказка, чтобы вы не гадали: страшное число — это действительно минимально возможное 32-битное целое число со знаком.

В Kotlin есть отличные extension-функции вроде .inc() для инкрементирования. Здесь всё выглядит просто. Что получится? Можем вызвать её на Int, и можем напечатать результат.

a) -2147483647
b) -2147483649
c) 2147483647
d) Ничто из перечисленного

Запускаем!

Но почему Long? Как видно из сообщения об ошибке, здесь проблема с Long.

Если inc() убрать, то это будет Int, и всё будет работать. У extension-функций приоритет, и компилятор сначала запускает inc(), а уже затем оператор минус. Получается Long, и только потом вызывается минус. Но inc(), запускаясь первым, превращает 2_147_483_648 в Long, потому что это число без минуса — это уже не валидный Int. Это всё уже нельзя передать в функцию printInt(), потому что она требует Int.

Если мы поменяем вызов printInt на обычный print, который может принимать и Long, тогда правильным будет второй вариант.

Берегитесь этого: далеко не на все паззлеры можно напороться в реальном коде, но вот на этот можно.
Мы видим, что это на самом деле Long.

Паззлер №3

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

В Kotlin 1.3 пришли новые отличные фичи. Кроме финальной версии корутин, мы
теперь наконец-то имеем unsigned-числа. Это нужно, особенно если вы пишите какой-то сетевой код.

Напоминаю, что Int у нас со знаком. Теперь для литералов даже есть специальная буква u, мы можем определять константы, можем, как в примере, декрементировать x и конвертировать в Int.

Что же получится?

a) -1 4294967294
b) 0 4294967294
c) 0 -2
d) Не скомпилируется

4294967294 — это максимальное 32-битное число, которое может получиться.

Запускаем!

Правильный вариант b.

Вывелся результат декремента unsigned, а это максимальное от unsignedInt. Здесь, как и в предыдущем варианте: сначала на x вызывается toInt(), а только потом декремент.

Самое интересное, что если написать вот так, код не скомпилируется:

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

И для меня очень странно, что первая строчка работает, а вторая — нет, это нелогично.

Причем в предрелизной версии правильным вариантом был бы С, так что молодцы в JetBrains, что фиксят баги перед релизом финальной версии.

Паззлер №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)

На этот случай мы напоролись в реальном коде. Мы в компании Codeborne делали Coding Dojo, вместе имплементировали на Kotlin Game of Life. Как видите, на Kotlin не очень удобно работать с многоуровневыми массивами.

Все единички вокруг являются соседями, и от этого зависит, будет ячейка жить дальше или умрёт. В Game of Life важная часть алгоритма — определение количества соседей для ячейки. В этом коде можно посчитать единички и предположить, что получится.

a) 6
b) 3
c) 2
d) Не скомпилируется

Давайте посмотрим

Правильный ответ — 3.

В результате суммируются только первые три ячейки. Дело в том, что плюс с первой строчки перенесён вниз, и Kotlin думает, что это unaryPlus(). Если мы хотим написать этот код в несколько строчек, нужно перенести плюс наверх.

Запомните, в Kotlin не надо переносить оператор на новую строчку, иначе он может посчитать его унарным. Это ещё один из «плохих паззлеров».

Это очень странная тема. Я не видел ситуаций, когда unaryPlus нужен в реальном коде, кроме DSL.

Если бы они были, было бы понятно, когда заканчивается одно выражение и начинается другое. Это цена, которую мы платим за отсутствие точек с запятой. Перевод строки для компилятора очень часто означает, что есть смысл попробовать рассмотреть строки отдельно. А без них компилятор должен сам принимать решение.

Но есть один очень классный язык JavaScript, в котором тоже можно не писать точки с запятой, и этот код всё равно будет работать корректно.

Паззлер №5

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

Этот паззлер засабмитил спикер KotlinConf Томас Нилд.

У нас nullable x, и мы можем его, если он окажется null, конвертировать через Элвис-оператор в какое-то нормальное значение. В Kotlin есть отличная фича nullable types.

Что будет?

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

Запускаем!

Если мы это зареформатируем, то официальный формат это сделает так: Проблема опять в порядке или приоритете операторов.

val sum = x ?: 0+y

Уже формат подсказывает, что сначала запускается 0+y, и только потом x ?:. Поэтому, естественно, остаётся 2, потому что икс равен двум, он не null.

Паззлер №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) val recipe = beer { name = ″Simple IPA″ hops { name = ″Cascade″ grams = 100
atMinute = 15 }
}

Когда меня позвали сюда, мне обещали крафтовое пиво. Я сегодня вечером пойду его искать, еще пока не видел. В Kotlin есть отличная тема — билдеры. Четырьмя строками кода мы пишем свой DSL и потом через билдеры его создаём.

Что у нас получилось? Мы создаем, во-первых, IPA, добавляем туда хмель, который называется Cascade, 100 грамм на 15-й минуте варения, и потом печатаем этот рецепт.

a) Recipe(name=Simple IPA, hops=[Hops(name=Cascade, atMinute=15, grams=100)])
b) IllegalArgumentException
c) Не скомпилируется
d) Ничего из перечисленного

Запускаем!

Хотели IPA, а получили «Балтику 7». Мы получили что-то похожее на крафтовое пиво, но в нем нет хмеля, он пропал.

Поле в Hops на самом деле называется kind, а в строке name = ″Cascade″ мы используем name, который заклэшился с name рецепта. Здесь произошел naming clash.

Теперь мы пытаемся запустить этот код, и он у нас не должен скомпилироваться. Мы можем создать свою аннотацию BeerLang и прописать его как часть BeerLang DSL.

DSLMarker для того и нужен, что внутри билдера компилятор не позволил нам использовать внешнее поле, если у нас внутри есть такое же, чтобы не было naming clash. Теперь нам говорят, что в принципе name нельзя из этого контекста использовать. Код исправляется так, и мы получаем наш рецепт.

Паззлер №7

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

Этот паззлер засабмиттил один из работников JetBrains. В Kotlin есть фича when. Она на все случаи жизни, позволяет писать крутой код, часто используется вместе с sealed-классами для дизайна API.

В данном случае у нас есть функция f(), которая принимает Boolean и что-то печатает в зависимости от true и false.

Что будет?

a) true TRUE; false FALSE
b) true TRUE; false TRUE
c) true FALSE; false FALSE
d) Ничто из перечисленного

Давайте посмотрим

Cначала мы вычисляем выражение x == true: например, в первом случае это будет true == true, что означает true. Почему так? А затем происходит ещё и сопоставление с образцом, который мы передали в when.

И когда x присвоено значение false, вычисление x == true даст нам false, однако в образце будет тоже false — так что пример будет соответствовать образцу.

Исправить этот код можно двумя способами, Один — убрать «x ==» в обоих случаях:

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

Второй вариант — убрать (x) после when. When работает с любыми условиями, и тогда не будет сопоставлять с образцом.

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

Паззлер №8

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

Kotlin продавали как «null safe»-язык. Представим, что у нас есть абстрактный класс, у него есть какое-то имя, а также property, которое возвращает logo этого языка: первую букву имени, на всякий случай сделанную заглавной (вдруг её забыли изначально заглавной сделать).

Что мы получим на самом деле? Раз язык null safe, мы поменяем имя и, наверное, должны получить корректное logo, которое является одной буквой.

a) K
b) NullPointerException
c) IllegalStateException
d) Не скомпилируется

Запускаем!

Проблема в том, что сначала вызывается конструктор суперкласса, код пытается проинициализировать property logo и взять у name нулевой char, а в этот момент name равно null, поэтому происходит NullPointerException. Мы получили NullPointerException, который не должны получать.

Самый лучший способ это исправить — сделать так:

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

Если мы запускаем такой код, мы получаем «K». Теперь базовый класс вызовет конструктор базового класса, он вызовет действительно getter name и получит Kotlin.

Property — отличная фича в Kotlin, но нужно очень аккуратно относиться, когда вы делаете override properties, потому что очень легко забыть, ошибиться или заоверрайдить не то.

Паззлер №9

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

Есть mutableList каких-то страшных вещей. Если вам это напоминает Scala, то это не зря, потому что действительно похоже. Есть List лямбд, мы берем два счетчика — I и j, инкрементируем и потом что-то с ними делаем. Что получится?

a) 1 1; 2 2; 3 3
b) 1 3; 2 3; 3 3
c) 3 1; 3 2; 3 3
d) ничто из вышеперечисленного

Давайте запускать

Так происходит, потому что i — переменная, и она сохранит свое значение до конца выполнения функции. Мы получаем 3 1; 3 2; 3 3. А j передаётся уже по значению.

Если бы вместо var i = 0 было бы val i = 0, это работало бы не так, но тогда бы мы не могли инкрементировать переменную.

Она очень крутая, но может нас укусить, если мы не сразу используем значение i, а передаем в лямбду, которая запускается позже и видит уже последнее значение этой переменной. Здесь в Kotlin мы используем замыкание, этой фичи нет в Java. А j передается по значению, потому что переменные в условии цикла — они все равно что val, своё значение уже не меняют.

В JavaScript был бы ответ «3 3; 3 3; 3 3», потому что там ничего не передаётся по значению.

Паззлер №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)

У нас есть функция foo(), берет два Boolean, печатает их, вроде всё просто. И у нас есть куча цифр, осталась посмотреть, какая цифра больше другой, и решить, какой вариант верный.

a) true, true
b) false, false
c) null, null
d) не скомпилируется

Запускаем

Не компилируется.

Хотя вроде как «c» — не класс, непонятно, почему у него должны быть дженерик-параметры. Проблема в том, что компилятор думает, что это похоже на дженерик-параметры: с<a,b>.

Если бы код был таким, он бы отлично работал:

foo(c > a, b > d)

Мне кажется, что это баг в компиляторе. Но когда я подхожу к Андрею Бреславу с любым таким паззлером, он говорит «это потому что парсер такой, не хотели, чтобы он был слишком медленным». В общем, он всегда находит объяснение, почему так.

Он сказал, что это исправлять не будут, потому что парсер в
Kotlin еще не знает про семантику. К сожалению, это так. К сожалению, так, наверное, и останется. Сначала происходит парсинг, а потом он дальше передаёт другому компоненту компилятора. Так что не пишите две такие угловые скобочки и любой код посередине!

Паззлер №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 — отличная фича в Kotlin. Кстати, Андрей Бреслав говорит, что это та фича, которую он бы с удовольствием из языка убрал, она ему больше не нравится. Сейчас, возможно, мы узнаем, почему! И ещё говорил, что companion objects некрасивые.

У нас есть data class Container, он берёт себе name и items. Но data classes — точно красивые. А заодно в Container мы реализуем тип items, это List, и все его методы мы делегируем items.

Мы «деструктурируем» элементы name и items из Container и выводим их на экран. Потом мы используем еще одну крутую фичу — destructure. Что получится? Вроде бы всё просто и понятно.

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

Запускаем

Он и оказывается верным. Самый непонятный вариант — d. Почему? Как оказалось, из коллекции items элементы просто пропадают, причем не с начала или с конца, а остаётся только посередине.

Я могу написать val (I, j) = listOf(1, 2), и получу эти 1 и 2 в переменные, то есть у List есть имплементированные функции component1() и
component2(). Проблема destructuring в том, что из-за делегации все коллекции в Kotlin тоже
имеют свой вариант destructuring.

Но так как второй компонент в данном случае приватный, выигрывает тот который публичный у List, поэтому из List берется второй элемент, и мы здесь получаем 2. У data class тоже есть component1() и component2(). Мораль очень простая: don’t do that, не надо так делать.

Паззлер №12

Следующий паззлер это очень страшный. Это засабмиттил человек, который как-то связан с Kotlin, поэтому он знает, что пишет.

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

У нас есть функция-расширение на nullable Any, то есть она может быть применена вообще на чём угодно. Это очень полезная функция. Если её ещё нет в вашем проекте, стоит добавить, потому что она может вам закастить всё, что угодно, во всё, что угодно. Потом мы берём 42 и кастим его в Nothing.

написать !!!!, компилятор Kotlin позволяет такое сделать: если вам не хватает двух восклицательных знаков, пишите хоть двадцать шесть. Ну, если мы хотим быть уверены, что сделали что-то важное, можно вместо!!!

Дальше мы делаем if (true), и дальше я сам уже ничего не понимаю… Давайте сразу выбирать, что получится.

Unit
c) ClassCastException
d) Не скомпилируется a) 87
b) Kotlin.

Смотрим

Скорее всего, Unit здесь получается из-за того, что туда больше нечего запихнуть. Здесь очень сложно дать логичное объяснение. Мы закастили что-то к Nothing, а это специальный тип, который говорит компилятору, что никогда не должен появиться instance этого типа. Это невалидный код, но он работает, потому что мы использовали Nothing. Компилятор знает, что если возникает возможность появления Nothing, что невозможно по определению, то дальше можно не проверять, это невозможная ситуация.

Фишка в том, что мы здесь обманули компилятор из-за этого каста. Скорее всего, это баг в компиляторе, команда JetBrains даже сказала, что, может быть, этот баг когда-нибудь исправят, это не очень приоритетно. и перестаём обманывать, то код перестанет компилироваться. Если убрать строчку 42.asGeneric<Nothing>()!!! А если оставляем, компилятор сходит с ума, думает, что это невозможное выражение, и запихивает туда что попало.

Может быть, кто-нибудь когда-нибудь объяснит это получше. Я так это понимаю.

Паззлер №13

У нас есть очень интересная фишка. Можно использовать dependency injection, а можно и без него обойтись, сделать синглтоны через object и круто запустить свою программу. Зачем нужен Koin, Dagger или что-то такое? Тестировать, правда, сложно будет.

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

У нас есть открытый для наследования класс A, он берёт что-то внутрь себя, мы создаём два object’a, синглтона, B и C, оба наследуются от A и передают туда друг друга. То есть отличный цикл образуется. Потом мы печатаем то, что B и C получили.

a) null; null
b) C; null
c) ExceptionInInitializerError
d) Не скомпилируется

Запускаем

Правильный вариант — C; null.

Но, когда мы это выводим, то у C не хватает B. Можно было бы подумать, что когда инициализируется первый объект, второго ещё нет. Это выглядит нелогично, логично было бы, наоборот, null; B. То есть получился обратный порядок: компилятор почему-то решил C инициализировать первым, а потом он инициализировал B уже вместе c C.

Такое тоже может быть. Но компилятор пытался что-то сделать, у него не получилось, он там оставил null и решил нам ничего не кидать.

в типе параметра убрать ?, то работать не будет. Если у Any?

нет, он кидает нам исключение, что нельзя сделать цикл.
Можно сказать браво компилятору за то, что когда null был разрешен, он постарался, но не получилось, а если?

Паззлер №14

В версии 1.3 вышли отличные новые корутины в Kotlin. Я долго думал, как бы придумать паззлер насчет корутин, чтобы его кто-то смог понять. Думаю, для некоторых людей любой код с корутинами — это паззлер.

3 поменялись некоторые названия функций, которые были в 1. В 1. Например buildSequence() переименован в просто sequence(). 2 в experimental API. То есть мы можем делать отличные последовательности с функцией yield, бесконечные циклы, и потом можем из этого sequence пытаться что-то достать.

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

С корутинами говорили, что можно все крутые примитивы, которые есть в других языках, типа yield, можно делать как библиотечные функции, потому что yield — это suspend-функция, которая может прерываться.

Что же будет?

a) [1, 2, 3]
b) [0, 1, 2]
c) Бесконечный цикл
d) Ничто из перечисленного

Запускаем!

Правильный вариант — последний.

А вот если добавить toList, вот тогда бы действительно вывелось [0, 1, 2]. Sequence — ленивая штуковина, и когда мы к ней цепляем take, она тоже ленивая.

Корутины действительно работают, их легко использовать. Правильный ответ вообще не связан с корутинами. Для функции sequence и yield даже не нужно подключать библиотеку с корутинами, все уже в стандартной библиотеке.

Паззлер №15

Этот паззлер тоже засабмитил разработчик из JetBrains. Есть такой адский код:

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

Когда я его первый раз увидел, во время KotlinConf, я не смог спать, пытался понять, что это такое. Вот такой криптический код можно писать на Kotlin, так что если кто-то думал, что Scalaz — это страшно, то на Kotlin тоже можно.

Давайте гадать:

Unit
b) Kotlin. a) Kotlin. Nothing
c) Не скомпилируется
d) Ничего из перечисленного

Давайте запускать

Мы получили Unit, который пришел неизвестно откуда.

Сначала мы присваиваем переменной лямбду: {->} — это валидный код, можно писать пустую лямбду. Почему? Соответственно, она возвращает Unit. У неё нет никаких параметров, она ничего не возвращает.

По факту она просто резервит Kotlin. Мы присваиваем переменной лямбду и сразу же пишем extension на эту лямбду, а потом ее же запускаем. Unit.

Потом на этой лямбде можно написать функцию-расширение:

.fun
Function<*>(){}

Она объявляется на типе Function, и то, что у нас сверху, ей тоже подходит. На самом деле это Function, но я Unit не писал, чтобы было непонятнее. Знаете, как работает звездочка в Kotlin? Это не то же самое, что вопросик в Java. Она выбирает тот тип, который лучше всего подходит.

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

На этом паззлеры завершаются. В заключение хочу сказать, что Kotlin — классный язык. Если вы iOS-разработчик и сегодня увидели его впервые, то увиденное не значит, что на Kotlin не надо писать!

Там без Kotlin тоже не обойдётся — доклад «Coroutining Android Apps» поможет не наломать дров при переходе к корутинам. Если вам понравился этот доклад с Mobius, обратите внимание: следующий Mobius состоится 22-23 мая в Петербурге. Будет и много другого для мобильных разработчиков (как Android, так и iOS), уже известные подробности о программе — на сайте, и с 1 марта стоимость билетов повысится.

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть