Чувак, где моя черепаха?
Как написать программу, чтобы победить на конкурсе плохого кода? Этот вопрос я задал сам себе, когда прочёл о необычном челлендже на форуме reddit. Да, вы правильно поняли. Это статья не о чистом коде и правильных тестах. Но здесь не будет и речи о плохом, заурядно плохом коде, том коде, который мы очень часто видим в наших проектах. Я расскажу об экстремальном, невообразимом, гениально плохом коде, коде, который использует те возможности джавы, о которых вы, скорее всего, и не догадывались, и те приёмы, которые вы никогда не встретите в обычных проектах. Сможете ли вы использовать эти приёмы на практике? Думаю, нет. Если вы прагматичный человек, то сэкономите своё время и остановитесь. Не читайте эту статью. Однако если вы хотите немного отвлечься от повседневной рутины, увидеть и узнать что-то новое о нашем любимом языке Java, — милости просим!
Bad Code Coding Challenge
Два года назад, пролистывая темы на сайте реддит, я случайно наткнулся на форум о плохом коде. Сам форум не показался мне интересным — мы и так каждый день видим образцы плохого кода. Какой смысл ими делиться? Но моё внимание привлёк необычный конкурс. Он назывался «Bad Code Coding Challenge». Каждые несколько недель создатель этого конкурса выкладывал условие задачи, и нужно было написать работающее решение на любом языке программирования, максимально избегая использования любых хороших практик. Оригинальный пост с подробными правилами и условиями вы можете посмотреть по ссылке, здесь же я приведу краткий перевод на русский язык задачи, которую я взялся решать.
Условия задачи
Нужно написать программу, которая принимает список инструкций для робота-черепахи и выводит ее координаты и направление после выполнения всех команд. Черепаха существует в бесконечной сетке координат. В начале выполнения программы она всегда находится в точке 0,0 и смотрит на север. Черепаха может смотреть на север, восток, юг или запад. Черепаха принимает две инструкции, каждая с аргументом. Команда forward
двигает черепашку вперед в ее текущем направлении на указанное количество шагов. Команда rotate
поворачивает черепаху по часовой стрелке на заданный угол. Поворот может осуществляться только на угол, кратный 90.
Задание очень простое, вполне для школьников, начинающих изучать программирование. Как написать хороший код? Создать несколько классов. Для команд, для черепашки. Отделить пользовательский интерфейс от бизнес логики… Ну, а плохой код? Никаких классов, все в одном методе main
! Всё должно быть свалено в одну кучу и максимально компакто. Мне пришла в голову мысль, что идеально плохая программа, вообще, должна быть написана в одну строчку, точнее в один стейтмент языка. Что ещё можно добавить к рецепту? Магических чисел и непонятных операций. Немыслимые преобразования типов… Да, классов у нас не будет, но есть простые типы. Можно придумать что-то необычное, то, чего никто и никогда не видел.
На удивление, у меня довольно быстро получилось набросать программку, которую я тут же запостил на форуме. Именно это решение вы можете найти в одном из комментариев к посту о черепашке.
public class Turtle {
public static void main(String[] args) {
System.out.println(java.text.MessageFormat.format(
"x = {1}, y = {0}, direction = {2, choice, 0#north|1#east|2#south|3#west}",
new java.util.Scanner(System.in).useDelimiter("\\R").tokens()
.mapToInt(s -> Integer.parseInt(s.substring(7).trim()) << s.charAt(0) / 'r' * ' ' / 2)
.mapToObj(data -> new Integer[] {(int)(short) data, (data >> 16) / 90})
.reduce(new Integer[]{0, 0, 0},
(turtle, action) -> {
turtle[2] += action[1];
turtle[2] %= 4;
turtle[turtle[2] % 2] += turtle[2] < 2 ? action[0] : -action[0];
return turtle;
})));
}
}
Так как у нас только один класс, нам не нужны сборщики и предварительная компиляция кода. Мы можем запустить программу, набрав в командной строке java Turtle.java
. Программа выполняет инструкции из стандартного потока ввода, если вы набираете команды из консоли, то для завершения ввода нужно нажать Ctrl+D
.
Первый вариант программы
Никакой проверки, как и положено плохому коду. Вы можете набрать инструкцию с ошибками, она всё равно будет обработана, главное чтобы первая буква была r
или f
.
Как и обещал, в коде есть необычное преобразование типов: (int)(short)
. При попытке что-то поменять, например, убрать одно из преобразований, программа перестаёт работать. Напишите мне в комметариях, как часто вы встречаете подобное в проектах на Java.
В поисках совершенства
Хотя здесь и были интересные находки, чем больше проходило времени, тем меньше мне нравилось моё решение.
Главная цель, которую я себе ставил, не была достигнута. Мы можем насчитать целых пять точек с запятой! У меня не получилось втиснуть алгоритм в один стейтмент…
Вторая оплошность — это имена переменных. Видимо подсознательно я хотел, чтобы код был понятен, и дал такие имена, как turtle
и action
. Не факт, что они проясняют смысл алгоритма, но их не должно быть совсем. Я решил, что в программе не должно быть имён переменных! Даже из одной буквы! Пусть будет лишь намёк, лишь символ…
Я решил повторить попытку. Взяв за основу первый код, я начал его менять. Я добавлял и убавлял, я крутил, вытягивал и снова сжимал код, пытаясь втиснуть его в одно утверждение. Почему-то, мне вспоминается одно из четверостиший Саши Чёрного:
Дочурка под кроватью ставит кошке клизму,
В наплыве счастья полуоткрывши рот,
И кошка, мрачному предавшись пессимизму,
Трагичным голосом взволнованно орет.
Я попытался сконцентрироваться, взял бумагу и нарисовал точку с запятой. Рой странных операций, магических чисел и функций кружил у меня в голове, но я бессилен был соеденить их вместе. Строчки Саши Чёрного не выходили из головы:
Я похож на родильницу,
Я готов скрежетать…
Проклинаю чернильницу
И чернильницы мать!
Тут одна идея, ярко брызнув искрами, осветила всё вокруг. «Ананасы в шампанском, ананасы в шампанском! Удивительно вкусно, искристо́ и остро́!», — восторженно заявляет ворвавшийся Игорь, полностью вытеснив Сашу. Я поспешно стучу по клавишам, стараясь зафиксировать пришедшую идею. «Стрёкот аэропланов! Бе́ги автомобилей! Ветропро́свист экспрессов! Крылолёт буеров!», безумная смесь классов, функций, тернарых операций и магических чисел сыплется непрерывным потоком, соединяясь в бесконечно длинное выражение. Я ставлю точку с запятой. Одну-единственную точку с запятой. Закрывающие фигурные скобки. «Я, гений Игорь Северянин, своей победой упоен…», — произносит поэт и исчезает.
Я сохраняю код класса в файл turtle.sh
и выставляю ему бит выполнения. Набираю в консоли ./turtle.sh
— программа работает.
#!/usr/bin/java --source 17
public class Turtle {
public static void main(String[] args) {
java.util.stream.IntStream.concat(java.util.stream.IntStream.of(0),
new java.util.Scanner(System.in).useDelimiter("\\R").tokens().mapToInt(
$ -> Integer.parseInt($.substring(7).trim()) * (1 - $.charAt(0) / "porkling"
.charAt(java.util.concurrent.ThreadLocalRandom.current().nextInt(8)) * 2)))
.reduce((₽, ₴) -> ₴ < 0 ? (₽ << 3 >>> 3) + (((₽ >>> 29) - ₴ / 90) % 4 << 29)
: ₽ + (₽ << 1 < 0 ? -1 : 1) * (₴ << (₽ << 2 < 0 ? 0 : 16)))
.ifPresent(¤ -> System.out.println(java.text.MessageFormat.format(
"x = {0}, y = {1}, direction = {2, choice, 0#north|1#east|2#south|3#west}",
¤ << 16 >> 16, ¤ << 3 >>> 19, ¤ >> 29)));
}
}
Снова смотрю на только что написанный код и осознаю, что почти не понимаю его. Неужели я смог достичь того идеала, когда автор перестаёт понимать свой собственный код не через неделю, а сразу, как поставил закрывающую фигурную скобку?
Показываю код коллеге. Просит объяснить, как это работает. Что ж, попробую. Там, где доллар и поросёнок («porkling»), команды преобразуются в биты и аккуратно выстраиваются в число. На ThreadLocalRandom.current::nextInt
можно не обращать внимания. Это всего лишь иллюзия случайности, что не мешает черепашке в точности выполнять команды. И при каждой команде, битики попадают в метод reduce
. Там они перемещаются налево и направо, умножаются и делятся, взаимно сокращая друг друга. При следующем повороте или движении черепашки новая партия битиков поступает в метод reduce
, и процесс идёт по кругу. Только когда последние биты сократят друг друга, мы сможем понять, где находится черепашка и куда она смотрит — на запад или на восток.
Легко понять и объяснить как работает чистый, хороший код. Значительно труднее, иногда даже невозможно понять и объяснить логику грязного, плохого кода. Я ещё раз посмотрел на программу. Код выглядел не просто грязным и плохим, он выглядел ужасным, каким-то зловещим. Нет, не нужно было участвовать в этом конкурсе. НЕТ ПЛОХОМУ КОДУ!
Эта статья — эксперимент. Если вы считаете статью посредственной, неинтересной — ставьте минус. Если же вы узнали что-то новое, если считаете, что статья этого заслуживает — плюсуйте. А пока вы выносите вердикт, я пойду писать о хорошем, чистом коде, о надёжных и правильных тестах и о том, как космические корабли бороздят большой театр.
Исходный код программы, тесты и описание даже не надейтесь найти в моём репозитории по адресу: https://github.com/rabestro