February 4th, 2020

Композируй это

Познакомился с Jetpack Compose. Это такой новый фреймворк «декларативного» UI для Андроида от Гугла. Потому что Гуглу, как обычно, мало одного нового «декларативного» фреймворка для Андроида (Flutter), и они решили делать сразу несколько. А чтобы ни кто не путался, делают их максимально похожими. Серьезно, авторы за ланчем обмениваются идеями и добавляют в оба.

Почему «декларативный» в кавычках? «Декларативные» UI-фреймворки — это новая волна, начатая Реактом и продолженная Flutter, SwiftUI, и вот теперь Jetpack Compose. Декларативные они в очень пост-ироничном смысле: упор в них как раз делается на описание UI в коде. А код, даже чисто функциональный, никак не может быть декларативным. Иерархия примерно такая:

  • Декларативный.
  • Чистый функциональный.
  • Императивный.

Понятно, что все эти фреймворки по уши сидят в императивности. Вот SQL декларативный. Prolog декларативный. Блин, даже XML/HTML/templates декларативные! А в том, чтобы позвать функцию, получить результат и обработать его, подергав попутно удобно подложенные глобальные процедуры, ничего декларативного нет. Но XML из моды вышел, А СЛОВО КРАСИВОЕ, чего слову пропадать?

Так вот, «декларативные» фреймворки, как их называют, содержат что-то вроде VDOM, а точнее концепцию top-down rerendering. Это когда твоя функция должна посчитать, как в текущий момент UI должен выглядеть, полностью с нуля (сгенерить VDOM), а задача фреймворка — привести UI в соответствие с этим представленим путем минимальных модификаций. В противовес старой ООП-UI модели, где эта задача ложилась на программиста и он неизбежно ныл и косячил. Раньше: panel1.close(); new panel2().open(), сейчас fun Window() = if (x) new panel1() else new panel2(); rerender(Window()); Мутабельный граф внутри все равно есть, только ходит с ним общаться сам фреймворк, ну, потому что код довольно однообразный был все равно.

Декларативность тут в том смысле, что фреймворк только описывает, как ему хотелось бы видеть UI, а не создает его самостоятельно. Ну натянуто, конечно, но хотя бы видна мысль. Почему первый «декларативный» фреймворк называется React, а не Declare, например, я если честно не знаю. Он ведь ни в одном месте даже не реактивный.

Так вот, особенно смешно в контексте спора за чистоту терминов смотреть именно на Compose, где весь API построен на мутейтящих глобальное состояние ПРОЦЕДУРАХ (ага, они еще бывают). То есть буквально () -> Unit. Код предполагается писать вот так (на выделение не смотрите, просто скриншотил в туториале):

Почему у Column такой большой отступ я вообще понятия не имею, в Котлине с форматтингом вообще плохо
Почему у Column такой большой отступ я вообще понятия не имею, в Котлине с форматтингом вообще плохо

Человек, опытный в Котлине, может заподозрить тут потенциальную возможность здравого смысла. Например, в Котлине есть все инструменты, чтобы Text() внутри, скажем, Column, на самом деле не просто вызывал функцию Text(), а дергал бы какой-то хитрый метод на внешнем скоупе (колумне как раз) с неявным ресивером и что-то куда-то там добавлял (количество неявностей в таком дизайне оставим без обсуждения). Но нет. Text(), как и все остальное здесь, – обычные процедуры. Зови в любом месте. Где позвал, там компонент и добавится. Императивочка-с.

Экспозиция закончилась, можно наконец перейти к сути поста. Идея делать такое АПИ — видимо, призрачное счастье разработчиков. Действительно, что может быть проще, чем написать:

Column {
   Button()
   Text()

Максимально лаконичное API, да? Но если присмотреться, мы увидим, что Column принимает не список детей, а замыкание. Почему это? Спросите вы. А потому что это гребаные процедуры, вот почему. Придумав такое API, ты невольно загнал себя в угол — его нужно держать строго определенным образом, и никак иначе. Удобно в простых примерах, а дальше начинаются сложности.

Мы, программисты, привыкли, что в коде можно делать много разных вещей. И нам это нравится! Можно принимать значения, передавать значения, трансформировать значения, обрабатывать значения. Но вы поняли, наверное, уже, к чему я веду, да? Компоуз решил нас этой радости, потому что его API ничего не принимает и не возвращает. Там нет значений. Его API можно только позвать, конец истории. Нельзя написать функцию, которая сортирует компоненты. Которая вставляет разделитель в список компонентов. Нельзя даже нормально вложить один компонент в другой без создания анонимной лямбды. Потому что и компонентов-то нет. Только лямбды. 

А лямбды это что? Это худший из возможных форматов хранения данных. Хуже структур, понятно, и даже хуже чем ООП. Лямбда — это черный ящик, черная дыра, максимально негибкое, неудобное и безполезное явление, на которое ни посмотреть, ни потрогать, ни сообщение послать, ни разобрать, ни распечатать, ни сравнить, а только обернуть в другую такую же гребанную лямбду или передать дальше. Лямбды невозможно упростить — они всегда только растут, толстеют, как слои лука у Шрека или жировые прослойки на твоей слоеной архитектуре. Максимально вредный объект, особенно как основа API. И поверьте, внутри Композа (да и снаружи) этих лямбд хоть лопатой жуй, и они все анонимные, и ничего не принимают и не возвращают, и от неявных ресиверов зависят, и друг друга заворачивают и разворачивают (ха-ха, шучу, лямбду нельзя развернуть!) и они анонимные все! А, это уже было. Ну вы поняли.

Лирическое отступление. Вообще общую адекватность автором можно оценить по вот такому интересному коду:

Это чтобы если ты пишешь a.ref=b то произошло бы на самом деле b?.value=a. Чем первое лучше второго я боюсь даже спрашивать. Серьезно, за психическое здоровье опасаюсь, если узнаю ответ.

Другой пример странного, кхм, изобретения, это то что они перегрузили (!) оператор (!!) унарный (!!!) плюс (!!!!) делать что-то нетривиальное. То есть оператор конечно довольно бестолковый, но все равно — что???

)

Так вот. Поинт. Насколько я понимаю, единственный смысл тащить дизайн UI в код — в том, чтобы работать с UI как с кодом. Потому что по всем остальным параметрам как раз декларативный XML выигрывает — дизайнер-визуализатор писать проще, анализ проще, оптимизаций доступно больше, перформанс лучше, быстрый релоад можно сделать. Но если ты от всего этого добровольно отказался, перешел в код, но все что ты можешь в этом коде делать — это аккуратно его держать, строить только специально оформленные функции и звать их в определенной последовательности, но никак не обрабатывать значения — зачем было заморачиваться? Это какое-то специальное программирование, очень странная дисциплина.

В каком-то интервью Артемий Лебедев объяснил, в чем проблема эргономичных вещей, не конкретных, а вообще, по жизни. Почему они не становятся мейнстримом. Потому что их создатели думают об одном каком-то, пусть даже основном, способе использования, и гипероптимизируют под него, забывая про все остальные. Так же и тут. Самый удобный АПИ у нас уже есть — это функции с явным списком аргументов и возвращаемым значением. И все. Конец истории. Лучше не будет. Серьезно. Не надо мудрить, хитрить, не надо придумывать «более удобный DSL», не надо аннотации придумывать, не надо компилятор форкать, не надо вообще программисту помогать вызывать функции. Лучше все равно не сделаете, а проблем на разгребание всего этого — создадите. Лучшая помощь — оставить нас в покое и дать писать обычный код. Будете проектировать свой UI-фреймворк — не ошибитесь.