Dap — еще один реактивный движок для веба. Совсем другой

в 9:58, , рубрики: ajax, angular, html, javascript, React, reactive programming, vue, веб-дизайн, Программирование

Хочу рассказать про dap — интересный и необычный язык реактивных правил для написания, в частности, веб-фронтендов.

Для затравки простая задачка: взять список неких пользователей (воспользуемся тестовыми данными, любезно предоставляемыми сервисом jsonplaceholder.typicode.com) и вывести их имена обычным html-списком; при нажатии на имя пользователя — показать алерт с его id.

Это, конечно, легко делается и на React, и на Angular, и на Vue. Вопрос: насколько легко? В dap это делается так:

'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users"
	,'LI'.d("! .name").ui("? .id:alert")
)

(*Этот и последующие dap-примеры можно интерактивно потестить в песочнице dap.js.org/test.html)

Это первая пришедшая в голову тривиальная задачка и тривиальный же способ ее решения. На dap удобно писать «в лоб», не городя огород из классов, компонентов и прочего ритуального реквизита. Что вижу, то пою. Но «пою» не на javascript или его производных, а на языке dap-правил, специально заточенном на простое и лаконичное описание реактивных зависимостей между элементами.

В примере выше, правда, никаких зависимостей пока нет. Зато есть:

  • оператор * для итерации по данным,
  • конвертер :query для асинхронной «конвертации» урла в полученные с него данные,
  • оператор ! для «печати» в генерируемый элемент,
  • конвертер alert, магически конверитрующий любые значения в алерт

Если вы знакомы с HTML, то слова UL и LI вам, вероятно, известны. Да, это теги для ненумерованного списка и для элемента списка, соответственно. А если и CSS вам не чужд, то и смысл записи UL.users вам должен быть понятен: это элемент UL с классом «users». На HTML это писалось бы <UL class="users">, но нам эти сложности с атрибутами и угловыми скобками ни к чему — dap использует лаконичную нотацию, очень похожую на CSS. Хотя и атрибуты при надобности в этой нотации возможны, например: 'FORM action=send.php method=post'.

Кстати: вы заметили, что этот пример написан на обычном javascript, а сами dap-правила это просто строки? И сигнатуры элементов («теги») — тоже строки, просто в одинарных кавычках. Да, код dap-приложения выглядит непривычно, но это — чистый javascript, готовый к употреблению: его не нужно транспилировать из исходников на новомодных языках (что, конечно, не помешает вам этими модными языками пользоваться при желании).

Чуть усложним задачку, добавив зависимостей: пусть при нажатии на имя пользователя его id и name отображаются крупненько в элементе H2, под ними пусть будет имейл, а также все его «посты», которые берутся из /posts?userId={id выбранного пользователя}.

'heroes'.d("$user=" // изначально $user не выбран

	,'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users" // для каждого пользвателя из списка
		,'LI'	.d("! .name") // вывести содержимое поля .name
			.ui("$user=$") // при нажатии выбрать этого пользователя
	)
	
	,'details'.d("*@ $user" // для выбранного $user выполнить следующее:
		,'H2'.d("! .id `: .name") // вывести его .id и .name,
		,'A'.d("!! .email@ (`mailto: .email)concat@href") // дать активную ссылку на .email,
		
		,'posts'.d("* (`https://jsonplaceholder.typicode.com/posts? .id@userId )uri:query" // и показать его посты
			,'H3'.d("! .title")
			,'p'.d("! .body")
		)
	)
)

Здесь уже имеем зависимость содержимого элемента 'details' (что в переводе н HTML означает <DIV class="details">) от того, какой пользователь выбран. Выбранный пользователь, таким образом, оказывается переменной состояния. Такие переменные в dap-правилах обозначаются префиксом $ (как s в «state»). При нажатии на любой из элементов LI изменяется содержимое переменной $user, на что элемент 'details' автоматически реагирует и обновляется.

В dap структура программы определяет сразу и модель состояния и модель взаимодействия с пользователем (отображение и реакции). С одной стороны это позволяет писать красивый, лаконичный и понятный код, но с другой стороны — требует четкой иерархической структуры приложения. Лично я считаю это требование полезным ограничением: оно заставляет (и помогает) более тщательно прорабатывать логику приложения и избавляться от логических нестыковок.

Каким образом 'details' узнает, что надо бы обновиться? Очень просто: в его правиле генерации присутствует обращение к переменной $user, а в правиле реакции элемента LI эта переменная как раз и изменяется.

Правила генерации — те, которые указываются с помощью метода .d — исполняются на фазе построения элемента. Правила реакции задаются методом .ui и исполняются при взаимодействии пользователя с элементом. Это два самых часто употребимых типа правил; кроме них есть еще несколько типов, но о них как-нибудь потом.

Синтаксис dap-правил довольно специфичен. Он не имеет ничего общего с всем привычным C-подобным синтаксисом, поэтому поначалу может казаться странным и непонятным. Но на самом деле он исключительно прост и, не побоюсь этого слова, прекрасен.

В языке нет ключевых слов. Зарезервированы символы .,$@:`(){} и пробел, все остальное может использоваться свободно. В частности, идентификаторы могут содержать, или состоять целиком, например, из символов !?* и т.п. Кому-то это покажется дикостью, но на деле это очень, очень удобно. Например, самыми часто используемыми в dap являются операторы (кстати, имена операторов — тоже идентификаторы):

  • ! — вывод значения в элемент, что-то вроде print
  • !! — установка свойства (атрибута) элемента, что-то вроде setAttribute
  • ? — условный оператор, что-то вроде if
  • * — мультиплексор, или итератор, что-то вроде for

Имена штатных операторов намеренно выбраны такими — невербальными. Во-первых, они просто короче — всего один символ. Во-вторых, хорошо отличимы от, скажем, имен констант и переменных. Ну и наконец, они нейтральны к локали и одинаково уместно выглядят в программах на любом национальном языке, хоть английском, хоть русском, хоть китайском (благо, javascript и unicode это позволяют).

А кто не сталкивался с ситуацией, когда сидишь и тупишь, придумывая имя для переменной? Хотите верьте, хотите нет, в моем dap-коде практически все «булевы» (да/нет) переменные именуются $? (где $ — это префикс переменной состояния, а собственно имя состоит просто из знака вопроса). Мне просто лень придумывать им имена, а потом еще и печатать их в нескольких местах. При этом никогда не возникает никаких сложностей с пониманием, что эта переменная означает в каждом конкретном месте: благодаря компактности и обозримости dap-кода, вся область действия такой переменной обычно умещается в несколько строк, и всегда понятно что к чему.

'toggle'.d("$?=" // определяем $? и инициируем ее 'ничем' (удобная разновидность false)
	,'BUTTON'.d("! `Toggle").ui("$?=$?:!") // кнопкой инвертируем $?, как в x=!x
	,'on'.d("? $?; ! `✓") // показать галочку если $? не пустой
)

Разумеется, это всего лишь мой личный стиль. Если вы пришли из мира Enterprise Java, не беспокойтесь: никто не запретит вам использовать сколь угодно длинные идентификаторы в любом месте. Чего нельзя сказать о литералах.

Литералы в dap-правилах обозначаются префиксом ` (backtick, на клавиатурах обычно под клавишей Esc). Например, элемент BUTTON в примере выше подписан литералом `Toggle. В отличие от других, «нормальных» языков, где, скажем, строковые литералы заключаются в кавычки и могут содержать приличные объемы текста, в dap-правилах литералы обрамляются только с одной стороны (тем самым префиксом `), и не могут содержать пробелов, т.к. пробел служит разделителем токенов («аргументов») в правиле. Как же так, спросите вы? А вот так. Литералы в dap предназначены не для эпистолярных фрагментов, а для различных коротких кодов: номеров, меток, каких-то отладочных заглушек и.п. Текстовые данные (как, впрочем, и любые другие) dap настойчиво требует хранить в виде констант в специальном словаре, в секции .DICT (от «dictionary», понятное дело):

'whoami'.d("$fruit="
	,'H3'.d("! ($fruit msg.fruit-selected msg.please-select)?! $fruit")
	,'UL'.d("* fruit"
		,'LI'.d("! .fruit").ui("$fruit=.")
	)
)
.DICT({
	msg	:{
		"please-select": "Кто я? Зачем я в этом мире?",
		"fruit-selected": "Я — "
	},
	fruit	:["Апельсинчик, сочный витаминчик", "Яблочко зеленое, солнцем напоённое", "Cлива лиловая, спелая, садовая", "Абрикос, на юге рос"]
})

Очевидным преимуществом хранения всех текстов в словаре является, например, легкость последующих локализаций на другие языки.

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

'multiselect'.d("$color= $size="
	,'H3'.d("! (($color $size)? (selected $color $size)spaced not-selected)?!")
	,'size'.d("$!=select(sizes@options)").u("$size=$!.value") // использовать шаблон select с данными из sizes
	,'color'.d("$!=select(colors@options)").u("$color=$!.value") // использовать шаблон select с данными из colors
)
.DICT({
	select: 'SELECT'.d("* .options@value" // этот шаблон используется выше
			,'OPTION'.d("! .value")
		).ui(".value=#:value"),
		
	sizes: ["XS","S","M","L","XL"],
	colors: "white black brown yellow pink".split(" "), // когда лень писать массив
	
	"not-selected": "Select size and color please",
	selected: "Selected specs:"
})

В принципе, ничто не мешает вообще все приложение разбить на «компоненты» и запихнуть в словарь, если вам нравится такой стиль:

'main'.d("! header content footer")
.DICT({
	header	:	'HEADER'.d(...),
	content	:	'UL.menu'.d(...),
	footer	:	'FOOTER'.d(...)
})

Лично я в этом смысла особого не вижу, и обычно выношу в словарь только те фрагменты, которые реально используются многократно.

Кстати, поскольку это все еще обычный javascript, а собственно словарь — это, как можно видеть, просто объект (или, как их еще называют, ассоциативный массив), то и формировать его можно любым легальным для javascript способом. Например, в примере чуть выше массив значений для константы colors генерируется из строки с помощью метода split. Можно вообще весь объект для словаря импортировать из внешнего скрипта-библиотеки любым доступным способом — хоть по старинке через <script src="..."> или XHR->eval(), хоть через import (но убедитесь, что ваши клиенты этот новомодный способ поддерживают). Секций .DICT может быть несколько, все они объединяются в один общий словарь.

const lib1={
		message: "Hello from lib1"
	},
	lib2={
		message: "Hi from lib2"
	};
	

'my-app'.d("! message wrapped.message imported.message")

.DICT(lib1)
.DICT({
	wrapped: lib2,
	imported: import "extern.js";
})

//файл extern.js
({
	message: "Bonjour de lib importé"
})

Помимо штатных средств javascript, для подключения внешних библиотек в dap имеется и свой механизм. Отличие его в том, что библиотеки подгружаются лениво — только тогда, когда действительно нужно что-то из библиотеки взять. Такие библиотеки указываются в секции .USES

'main'.d("$?=" // unset $? 
	,'BUTTON.toggle'.ui("$?=$?:!") // toggle $?
	,'activated'.d("? $?" // only show when $? is set
		,'imported'.d("! delayed.message") // now print the message from the lib
	)
)
.USES({
	delayed: "extern.js"
})

Здесь библиотека extern.js загрузится только после нажатия кнопки, когда потребуется отобразить элемент 'imported' — а для этого распарсить и скомпилировать его dap-правило, которое и ссылается на внешнюю библиотеку.

Да, dap-правила «компилируются» перед исполнением. Причем лениво, только при первом фактическом обращении к шаблону элемента. Эта ленивость позволяет «размазать» компиляцию большого приложения на множество мелких этапов, и не занимать ресурсы браузера неиспользуемыми участками кода. Конечно, актуальна такая забота о ресурсах только для каких-то совсем уж больших приложений, или слабых устройств. В целом могу сказать, что dap молниеносен (в масштабах UI, конечно) — и в компиляции, и в исполнении. Производительность можно контролировать в консоли браузера: там логируется время каждой реакции.

Единственные заметные задержки, которые реально могут возникать — это задержки сети. Но и эти задержки не блокируют dap-приложение. Веб-запросы, исполняемые конвертором :query асинхронны и не задерживают отображение и работу остальных элементов.

Кстати, что такое конвертор? Конвертор в dap — это просто функция вида value → value, то есть с одним входом и одним выходом. Такое ограничение позволяет строить из конверторов цепочки, например выражение $a:x,y,z соответсвует z(y(x($a))) в си-подобной записи. То, что вход у конвертора всего один, казалось бы, ограничивает его возможности по сравнению с «обычной» функцией. Но это не так. Конвертор может принимать и отдавать как элементарные значения, так и объекты/массивы (в javascript разница между этими понятиями размыта), содержащие любое количество данных. Таким образом, конверторы в dap полностью заменяют «традиционные» функции, при этом могут собираться в цепочки и могут быть асинхронными, не требуя при этом никакого дополнительного синтаксического оформления.

Традиционных «функций» с фиксированным списком параметров в dap, соответственно, нет — за ненадобностью.

Зато есть еще агрегаторы — функции от произвольного числа аргументов (обычно равноправных, но не всегда). Например, агрегатор ()? возвращает первый непустой аргумент или самый последний (аналог ||-цепочки в javascript), а агрегатор ()! — наоборот, первый пустой аргумент, или самый последний (аналог &&-цепочки в javascript). Или, например, агрегатор ()uri — строит из аргументов параметризованный URI.

И последний тип функций в dap — это операторы. Если конверторы и агрегаторы идеологически ближе к «чистым» функциям, которые просто вычисляют значения (хоть и не всегда это так), то задача операторов — применять эти значения к генерируемому элементу (например, оператор ! добавляет значение аргумента к содержимому элемента, а оператор !! устанавливает атрибуты элемента) или управлять ходом исполнения правила (как, например, условный оператор ?)

Арсенал штатных конверторов, агрегаторов и операторов в dap минимален, но это не важно. Главное, что вы можете создавать свои! Это важный момент. Нужно четко понимать, что сам язык dap-правил вовсе не претендует на роль универсального языка программирования, он только описывает зависимости между элементами. Подразумевается, что хардкор и вычисления реализуются нативными средствами среды (для браузера, это, понятно, javascript) а dap играет сугубо «логистическую» роль — указывая, что, когда и из чего нужно делать и куда отдавать.

Собственный функционал описывается в секции .FUNC (от «functionality»):

'UL.users'.d("* :query`https://jsonplaceholder.typicode.com/users"
	,'LI'.d("! (.name .username:allcaps .address.city):aka,allcaps")
		.ui("(.address.street .address.suite .address.city .address.zipcode)lines:alert")
)
.DICT({
	"no-contact": "No contacts available for this person"
})
.FUNC({
	convert: {
		allcaps: o=> o.toUpperCase(),
		aka	: o => o.name + " also known as "+o.username+" from the city of "+o.city
	},
	flatten:{
		lines : values=>"Address:n" + values.reverse().join("n")
	}
})

Здесь собственный функционал тривиален, поэтому написан непосредственно «по месту». Разумеется, что-то более сложное имеет смысл писать самостоятельным модулем, а в .FUNC прописывать только протокол взаимодействия между этим модулем и dap. Скажем, в примере с игрой в крестики-нолики dap.js.org/samples/tictactoe.html (по мотивам React-туториала), вся логика игры описана в отдельном замыкании, а dap только связывает эту логику с картинкой на экране.

Подробнее познакомиться с dap можно на сайте dap.js.org

Да, вот еще что. В сжатом виде весь dap-движок (<script src="https://cdn.jsdelivr.net/gh/jooher/dap/0.4.min.js">
) весит ни много ни мало 9 кБ. Девять килобайт.

Автор: jooher

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js