Хочу рассказать про 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 importe"
})

Помимо штатных средств 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 кБ. Девять килобайт.

Комментарии (25)


  1. Sirion
    02.12.2019 13:34

    Как у посетителя известного дочернего ресурса Рутрекера, у меня своеобразные ассоциации с названием библиотеки)


    1. jooher Автор
      02.12.2019 17:26

      Что поделать, Rule 34 никто не отменял


  1. JustDont
    02.12.2019 13:38
    +7

    — Давайте загоним логику в string, а потом мужественно будем её отлаживать?
    — Отличная идея!

    Неструктурированная и некомпилируемая AOT шаблонизация — это so 2012. Тогда это казалось модно и молодёжно. К 2019 году пора бы уже понять, что это не масштабируется. Вообще. Совсем.


    1. jooher Автор
      02.12.2019 17:37

      В правилах прописывается только относительно простая логика взаимосвязи между элементами. Хардкор, требующий пошаговой отладки, пишется отдельно на js (или ts если хотите), дап просто обращается потом к этому коду.


      1. JustDont
        02.12.2019 18:55
        +3

        Она «относительно простая» только пока вы пишете с её помощью «относительно простые» вещи.
        Алсо, отвечая на комментарий ниже, если ради структуры мне руками предлагается структурировать стринги как надо, то зачем мне вообще тогда стринги (читай: dap) in the first place?


        1. jooher Автор
          02.12.2019 20:00

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


          1. JustDont
            02.12.2019 20:52

            Либо я не понимаю, что вы имеете в виду под «структурировать стринги»

            Задать им тип, в простейшем случае. Или хотя бы открыть их для статического анализа (инструментом, который еще написать кому-то придётся). Иначе каждая опечатка превращается или в падение шаблона, или, что в разы хуже, в его тихую неправильную работу.


            1. jooher Автор
              02.12.2019 21:07

              Кому "им"? Что вы стрингами называете? Правила, которые в виде простых строк пишутся или данные, с которыми дап работает?
              Если первое, то непонятно какой тип вы "им" хотите задать.
              Если второе — то при чем тут стринги вообще? Сам дап работает с любыми js-данными, которые вы ему дадите. Он ими вообще не интересуется, а только передает между "участниками процесса" как есть, без всяких стрингов. А уж как их трактовать и какие типы кому назначать — это вы сами решаете.


              1. JustDont
                02.12.2019 21:27

                Правила, которые в виде простых строк пишутся или данные, с которыми дап работает?

                Вы искренне не понимаете, или пытаетесь, пардон за мой французский, играть в дурачка? Кто в вашем чудесном мире называет данные «стрингами»?

                Если первое, то непонятно какой тип вы «им» хотите задать.

                Соответствующий их назначению. Тип соответствующего элемента DOM, если речь про html. Тип данных, если речь идёт про ссылку на оные, и так далее. Нормальная система шаблонизации должна быть анализируема, а еще лучше — статически типизирована, иначе всё обсуждение крутости таких систем — это разговоры в пользу бедных; несколько опечаток в тысячах этих строк превращают результат в тыкву.


                1. jooher Автор
                  02.12.2019 21:52

                  В моем чудесном мире полно разных людей, называющих стрингами совершенно разные вещи. И под "запихнуть в стринги" понимающих тоже разное. Так что не всегда и угадаешь.
                  Где написано, что такое "нормальная система шаблонизации" и что она кому "должна", не подскажете?


        1. jooher Автор
          02.12.2019 20:09

          Алсо, если говорить про UI, там взаимосвязи между элементами кагбе и должны быть "относительно простыми" с т.з. дапа по крайней мере (хоть это и не обязательно будет просто с т.з. реакта или, прости г-ди, jQuery). Из моего опыта, если правила получаются сложные и запутанные — обычно это сигнал о плохой логике самой связи.


    1. jooher Автор
      02.12.2019 17:38

      Структурировать шаблоны дап никак не мешает. Не требует, но и не мешает.


  1. nebsehemvi
    02.12.2019 17:04

    Напомнило D3.js.


    1. jooher Автор
      02.12.2019 17:04

      Чем?


      1. nebsehemvi
        03.12.2019 09:16

        Вместо блоков кода — цепочки.

        d3.json("https://jsonplaceholder.typicode.com/users").then(res => {
        d3.select("body")
          .append("ul")
          .selectAll('li')
          .data(res)
          .enter()
          .append("li")
          .text((d) => d.name)
          .on('click', (d) => alert(d.id));;
        });
        


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


        1. jooher Автор
          03.12.2019 10:46

          Нет. Ничего общего, кроме того, что и тут и там присутствуют вызовы методов. Ну так они почти в любом js-коде присутствуют. В d3 цепочка методов последовательно выполняет конкретные действия над контекстом исполнения. В dap методов всего несколько (во всех примерах выше их только два: .d и .ui), и они просто назначают шаблону правила. А весь движ идет уже в этих правилах и вызываемых ими функциях.


  1. dagen
    03.12.2019 16:12
    +3

    Продолжу тему ассоциаций. Напомнило $mol. Точно так же написано черт-те как, происходит черт-те что, и никому читать такой код не хочется.


    1. JustDont
      03.12.2019 16:28
      +1

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


  1. justboris
    04.12.2019 00:46

    А вы на hyperscript смотрели?


    Очень похоже на то что у вас, только без магического синтаксиса.


    1. jooher Автор
      04.12.2019 01:40

      Да, сходства есть: DOM строится из JS вместо HTML, и нотация элементов похожа на CSS. На этом, пожалуй, все. И в dap это совсем не главное. Изначально (2008 г, dapmx.org) dap был на HTML, потом на XML, и только недавно от этого всего избавился в пользу чистого JS.
      Магический синтаксис — это собственно дап и есть.


  1. CR33P
    04.12.2019 01:40

    Кто-то, кроме самого разработчика jooher этим пользовался? Есть фидбеки по процессу боевой разработки?


    1. jooher Автор
      04.12.2019 01:59

      Нет, я его, можно считать, еще не публиковал. Пользуюсь сам для своих нужд около 10 лет.
      Из примеров:
      bankreports-dapmx-o.1gb.ru (самое первое dap-приложение 2008-го года, ныне заброшеное),
      bazamagaza-com.1gb.ru (интернет-магазин/склад/crm в одном флаконе, тоже достаточно древний)
      dapmx.org/apps/rockauto (простой фронтенд к магазину автозапчастей)

      Боевые проекты в открытый доступ дать не могу. т.к. там живые клиенты с данными.

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


  1. KwI
    04.12.2019 07:18

    Привет! Обращусь вот к этой строке:
    :query`https://jsonplaceholder.typicode.com/users"
    Замечательно, что библиотека берет взаимодействие с сетью под капот. Однако может у меня на работе что-то не так — ux-правила требуют, чтобы при запросе показывался прелоадер, в случае ошибки корректно выводилось её содержание пользователю и дальше всякие-всякие сценарии развития ситуации.
    Можно ли такое провернуть в dap? Если да — то насколько оно сложнее выглядит по сравнению с этой маркетинговой строчкой?


    1. jooher Автор
      04.12.2019 17:39

      В dap можно провернуть все, что можно провернуть в js. Dap — это не «язык вместо js», а просто средство передачи данных от одного элемента другим.

      Что касается обработки запросов.
      Конвертор :query — для ленивых (лично я пока реально пользовался только им). Он получает данные, парсит их в соответствии с mime типом и отдает результат. Если что-то пошло не так, он просто отдает «ничто». Если вам не важно, что именно пошло не так (обычно так и есть, это нормальное для браузера поведение), и достаточно просто выдать ошибку вида «ой!», то это будет что-то вроде:
      ? $data=myDataUrl:query errorMessage:alert; .... используем $data ....

      Если вам надо прям все знать, то можно воспользоваться более низкоуровневым конвертором :request, который просто выполняет запрос и отдает его как есть, со всеми потрохами:
      $req=myDataUrl:request; ! $req.status $req.headers и т.д.

      Если вы хотите вообще как-то хитро запросы делать, можете сделать свой конвертор, с блэкджеком прогрессбаром и алертами и пользоваться им: $data=myDataUrl:myFancyDataLoader

      В принципе это тема для отдельной статьи. Не конкретно обработка ошибок загрузки, а использование собственных функций. То, что дапа будет выглядеть простым конвертором, на деле вполне может быть жыыырным черным ящиком, именно спрятанным под капот.


  1. OldVitus
    05.12.2019 02:13

    Было бы интересно посмотреть решение на dap стандартной задачи типа Todo-MVC, чтобы было с чем сравнить.