Как связаны Atomic CSS и Функциональное программирование
Почему подход Atomic CSS называют функциональным? Именно на этот вопрос я постараюсь ответить в данной статье! Сначала я опишу вам базовые принципы ФП, а затем расскажу про основы Atomic CSS, проводя аналогии с функциональным программированием.
Всем привет, друзья!
Меня зовут Рамазан, я Frontend-разработчик и энтузиаст, который любит искать свежий взгляд на привычные вещи в Web-разработке.
Думаю, что вы слышали когда-либо про функциональное программирование (далее - ФП). Это парадигма для которой характерно применение чистых функций и сохранение иммутабельности данных. Существует ряд языков, в которых принципы ФП преобладают: Haskell, OCaml и Elixir. Однако и другие языки вроде JavaScript, Python, C++ поддерживают этот подход, хотя только им не ограничиваются.
Но внимательный читатель посмотрит на название и спросит: "Функциональное программирование - это хорошо. А причём здесь Atomic CSS?". Сейчас отвечу! Всё дело в том, что ещё на заре появления атомарного подхода у него существовало и другое название - Functional CSS. Кто-то довольно часто использует его и сейчас, чтобы не путать с одноимёнными терминами. Но почему этот подход к CSS-стилизации называют функциональным?
Именно на этот вопрос я постараюсь ответить в данной статье! Сначала я опишу вам базовые принципы ФП, а затем расскажу про основы Atomic CSS (далее - ACSS), проводя аналогии с функциональным программированием. Кроме того, я постараюсь на простых примерах показать, какие проблемы решаются, если применять Atomic CSS при стилизации. Когда я готовил материалы для этой статьи, я много опирался на этот и на этот доклады. Наконец, хотел заметить, что это перевод моей оригинальной статьи на английском языке — так что можете глянуть и её.
Ну что, держитесь крепче - мы начинаем!
Что нужно знать
Чтобы разобраться в этой статье, все, что вам нужно, - это базовое понимание HTML, CSS и JavaScript. Также в статье имеется несколько примеров, в которых мы будем использовать Atomic CSS фреймворк [mlut](https://mlut.style/), но вам не обязательно знать его синтаксис, потому что я привёл расшифроку его утилит в рукописном CSS.
Базовые принципы ФП
Функциональное программирование - это большая область, в которой написано немало сложных статей и которой посвящен целый научный журнал. Поэтому я в своей статье сосредоточусь на том, чтобы изучить только базовые принципы ФП и провести аналогию с ними в Atomic CSS.
Этот подход основывается на том, что все необходимые для нас действия в программе мы должны совершать посредством вызова некоторых функций и их композиций.
Давайте я приведу основные понятия, через которые попытаюсь раскрыть суть данного подхода:
1) Чистые функции;
2) Иммутабельность;
3) Композиция функций.
Чистые функции
Функция называется чистой, если она:
- при одних и тех же входных параметрах возвращает одно и то же значение;
- не имеет побочных эффектов (изменений внешних значений или сущностей).
Приведу для наглядности пару примеров.
Первая функция чистая, ведь, если мы будем вводить одни и те же аргументы, то будем получать один и тот же результат. Кроме того, никакие глобальные переменные эта функция не меняет, а объекты не мутирует.
Вторая функция не попала в понятие чистоты по обоим пунктам. Она меняет внешнюю переменную `s` и использует для вычислений другую внешнюю переменную `c`, которая может меняться.
Чистые функции позволяют писать более предсказуемый код. Приложение может разрастись, а функция с неявным параметром, который может меняться со временем, может привести к большим сложностям в дебаге и поддержке кода.
Иммутабельность
Иммутабельность - это принцип, в соответствии с которым объекты данных не должны меняться после создания. Чтобы произвести изменения над данными, необходимо создать новый их экземпляр и работать с ним.
Поначалу может показаться, что таким образом мы лишаем себя гибкости в процессе разработки и уменьшаем скорость работы программы. Но в действительности, если в языке или рантайме есть оптимизации для иммутабельных данных, соблюдение этого принципа помогает избегать многих ошибок и использовать параллельные вычисления.
Вот довольно простой пример: возьмём компонент React, который отображает задачу в списке дел. Состояние задачи описывается объектом. И чтобы React правильно перерисовал состояние компонента задачи, когда пользователь что-то в нём изменяет, необходимо передать в качестве нового состояния не измененный старый объект, а новый экземпляр объекта с текущим состоянием. Вот пример, где для состояния задачи используется иммутабельный объект:
Здесь мы увидим, что нажатие на кнопку приведёт к изменению состояния задачи и вызовет повторную прорисовку компонента. Однако мы могли бы определить функцию `toggleDone()` иным способом, используя мутации объекта:
При использовании такого обработчика событий эффект не будет достигнут, поскольку ссылка на объект остаётся прежней, несмотря на то что сам объект был изменён.
Композиция функций
Композиция функций подразумевает применение результата выполнения одной функции в качестве аргумента другой функции. Приведём пример программы, которая делает первую букву каждого слова заглавной:
Здесь мы определили функцию `compose(f1, f2)`, которая возвращает композицию функций, переданных ей в аргументах. Дальше мы используем эту функцию для создания функции `capitalize()`, которая будет как раз делать только первые буквы слов заглавными посредством композиции функций `lower()` и `upperEveryFirst()`. Сначала выполняется первая из них, и она возвращает строку со всеми строчными буквами в аргумент второй функции. Вторая же функция делает первую букву каждого слова заглавной.
В больших проектах и при необходимости сложных вычислений такие композиции могут быть и несколько больше, а логика в них может быть куда более богатой. И в этих случаях такой подход к проведению вычислений помогает разбить очень объёмные преобразования на серию относительно простых и компактных функций, которые применяются одна за другой. Это повышает удобство разработки, рефакторинга и отладки кода.
Как принципы ФП находят отражение в Atomic CSS
Теперь, когда мы узнали немного про функциональное программирование, попробуем ответить на вопрос: "А причём здесь Atomic CSS?". Проведём аналогию между этими подходами на уровне описанных выше принципов.
О том, что такое Atomic CSS
Но сначала уделим немного времени тому, что такое Atomic CSS. Это методология вёрстки, в которой мы используем маленькие атомарные CSS-правила, каждое из которых делает одно действие. Эти классы называют утилитами. Обычно они применяют одно CSS-свойство, но не обязательно одно. Например, во фреймворке [mlut](https://mlut.style/) утилите `Bgc-red` соответствует свойство `background-color: red`, а утилите `-Sz50p` - сразу два свойства `width: 50%` и `height: 50%`.
Современные Atomic CSS фреймворки, типа mlut и Tailwind, внутри используют так называемый JIT-движок. Это компонент, который генерирует CSS на основе только тех утилит, которые вы использовали в разметке.
Чистота
Чистота в CSS определяется тем, какими селекторами, а если конкретнее - классами, задаётся стилизация элементов. В чистом CSS поведение элемента должно определяться исключительно теми классами, которые привязаны к нему в атрибуте `class`. То есть в файле со стилями в идеале не должно быть селекторов вроде `section`, `div > ul`.
Во-первых, они задают слишком общие правила, которые с большой вероятностью должны быть нарушены или дополнены в той или иной части проекта. Поэтому, когда мы будем стилизовать конкретные элементы, нам придётся постоянно держать в голове эти стили, чтобы понять, как аккуратно достичь необходимой стилизации, ничего не испортив.
Во-вторых, нарушается чистота CSS для каждого конкретного элемента. Допустим, мы имеем следующую разметку:
А CSS будет таким:
В итоге мы получим, что первая кнопка окрашивается в красный цвет, а вложенная - в зелёный. На таком примере это кажется довольно безобидным, но это будет приносить неудобства, когда структура проекта сильно разрастётся. И здесь мы видим, что результат работы собственных стилей класса `.greeting` зависит от того, где соответствующий элемент находится. Это аналогично тому, что функция при одних и тех же вводных данных даёт разные результаты в зависимости от того, где вызывается.
Atomic CSS позволяет такого эффекта избежать. В этом подходе в большинстве случаев стили применяются только к тем элементам, для которых прописаны соответствующие классы. Если же необходимо сделать несколько одинаковых элементов, то одна и та же утилита прописывается в атрибут `class` каждого такого элемента.
Аналогичный пример можно переписать на mlut вот так:
Где JIT-движок mlut сгенерирует такие стили:
Здесь мы уже видим, что стили элементов в таком подходе будут зависеть только от классов, которые им приписаны.
Замечание: здесь, правда, стоит заметить, что синтаксис mlut позволяет делать вещи, которые отходят от строгого понятия чистоты CSS. Иногда это бывает необходимо для создания относительно более сложных эффектов. Допустим, мы хотим реализовать такую карточку, при наведении на которую меняется цвет фона у вложенной в неё кнопки. Тогда на mlut нужно будет написать следующее:
Иммутабельность
Под иммутабельностью CSS я буду подразумевать то, что стили элементов не переписываются. Иммутабельность в ACSS означает, что утилиты, как правило, не мутируют друг друга. Например, в BEM основные стили задаёт блок или элемент, а модификатор эти стили мутирует. А в других подходах, где используются комбинированные селекторы, это мутирование происходит чаще и менее явно.
Попробую привести простой пример. Пусть у нас есть карточка с товаром, которая может быть в своём дефолтном состоянии, либо в состоянии выделения. В BEM это бы выглядело примерно следующим образом:
В этом примере мы видим, что класс `product-card` задаёт красный цвет фона карточки по умолчанию. И, чтобы как-то обозначить выбранную карточку посредством другого цвета фона, нам приходится добавлять класс-модификатор, который изменяет цвет с красного на зелёный. И делает он это посредством переписывания свойства `background-color`, то есть с помощью мутирования стилей блока.
В подходе Atomic CSS эта проблема решается, так как утилиты позволяют задавать CSS-свойства независимо друг от друга и задавать модификации, не прибегая к мутации. Вот так будет выглядеть данный пример, если использовать mlut:
Композиция
В функциональном программировании повсеместно используются композиции функций. В атомарном CSS аналогом композиции функций служит композиция утилит при стилизации элементов. Как в ФП мы посредством последовательного применения множества простых функций получаем сложное поведение, так и в ACSS посредством множества простых утилит мы можем получить нетривиальную стилизацию.
Для примера покажу простой смайлик, который сделан посредством только лишь Atomic CSS:
Так будет выглядеть наш результат:
Таким образом, применяя утилиты одну за другой, мы получили даже небольшой CSS-арт.
Заключение
Подводя итоги, скажу, что Atomic CSS действительно воплощает в себе базовые принципы функционального программирования. Конечно, не буквально, но в том смысле, который актуален для Frontend-разработчиков и верстальщиков. Я был бы рад услышать ваши дополнения и возражения в комментариях - будет интересно их почитать и над ними подумать!
Напоследок скажу: смотрите на привычные вещи свежим взглядом!
И, как обычно, успехов вам в увлекательном пути Frontend-разработки!