Open-source блокнот Wolfram Language или как воссоздать минимальное ядро Mathematica на Javascript и не только
На Хабре уже проскакивали упоминания о совместимых или систем-копиях Wolfram Mathematica, но реализованных на других языках, как, скажем, Mathics. Автор статьи @ITSummaупомянул в самом начале
На Mathics такое не получится, как и многие другие примеры из этого списка тоже не сработают. Вообще, для Wolfram Language (WL) практически невозможно создать полноценный интерпретатор с открытым исходным кодом, потому что многие встроенные решатели являются проприетарной собственностью компании Вольфрама. Однако попытаться можно.
Сложно поспорить с этим утверждением, однако, возможен компромиссный вариант, позволяющий использовать все те же "решатели", но с немного иной open-source оберткой снаружи. В качестве ответа я представляю систему, которая не только воспроизводит многие ключевые функции блокнота Mathematica с нуля, но и расширяет функционал гораздо дальше, чем там, где очертил его границы Стивен Вольфрам, создав эту потрясающую систему более 30-ти лет назад.
::: Это не готовый продукт и не замена Wolfram Mathematica
Вставка для привлечения внимания
Здесь мне потребовалось завлечь пользователей этим замечательным корабликом. То, на чем он написан, - это ни что иное как Wolfram Language и то, где он исполняется здесь и сейчас, - Ваш браузер (для тех, кто кликнул на картинку).
документация (наполняется)
Но я обманул Вас, это не open-source блокнот, а нечто другое, но не менее важное, о чем Вы узнаете позже в секциях ниже. Начнем с привычных разделов...
Блокнот? Швейцарский нож
Код, иллюстрации, data-science, презентации - все сегодня возможно написать в пределах скевоморфиозного вида интерфейсов - блокнота
Ячейки разного типа это безусловно преимущество, особенно это касается типа Markdown, когда его "привезут" в Wolfram Mathematica - неизвестно.
Про презентации мы еще поговорим позже.
Бесплатный сыр
Важно отделять Wolfram Language от того, что его реализует - Wolfram Mathematica. Однако это также не совсем верно, так как Wolfram Mathematica это язык и интерфейс к нему (фронтенд), которые, вероятно, соразмерны друг с другом.
(Около-)Свободная реализация языка (см. комментарии про ограничения)со стандартными библиотеками уже давно доступна - это Wolfram Engine, который подобно Питону можно подключить в качестве скриптового языка к чему-угодно.
Отличия интерфейса блокнотов Mathematica от других
Некоторые из вас могут посчитать следующие пункты полезными или бесполезными конкретно для вашей работы или подхода к программированию, однако, нельзя опускать сам факт их реализации - это технически и концептуально сложная задача, которая была великолепно решена. Такое нельзя найти в Jupyter, Obsevable (d3-express), VSCode Notebook API.
Синтаксический сахар
Возведем идею формочек с цветом, которые многие видят в Visual Studio Code, редактируя какие-нибудь CSS цвета
в бесконечность и получим
Graphics3D[Sphere[]]
% /. Sphere -> Cuboid
Здесь основная идея состоит в том, что сам график с точки зрения среды - это набор выражений и символов. Когда он рисуется на экране - это все еще тот же набор символов и выражений, с которым можно взаимодействовать. А трехмерный куб - это просто одна из возможных интерпретаций.
Выходные ячейки - редактируемы
Я не знаю почему, но почти все блокнотные интерфейсы просто игнорируют эту опцию
Мы получили результат - это тоже выражение. Почему бы не использовать его в последующих вычислениях?
Возможно, языков программирования, которые могли бы воспользоваться этим на благо просто мало.
Двумерный математический ввод
Здесь я явно предвзят, так как являюсь физиком-теоретиком. Что вам нравится больше?
1/Sqrt[2]
В редакторе Хабра это сложно показать, но возможность миксовать код и математические выражения подобные тем, что в LaTeX, - это потрясающе. Представьте, если обобщить это, писать код, вставлять изображения или другие интерактивные объекты в то время, как редактор будет это видеть и обрабатывать как все тот же код. Очевидно, это работа для регулярных выражений.
Зачем изобретать велосипед с открытым исходным кодом
Очевидный вопрос, ведь рынок уже удовлетворен тем, что создает WRI. Время привести недостатки
Wolfram Mathematica
Проприетарный формат/среда, который/ая стоит дорогоТяжелый интерфейс (в плане отзывчивости), нестабильный UI (краш, фриз это обычное дело)
Клиент и среда связаны, сложно вести удаленную сессию с телефона/тостера
Нельзя делиться блокнотами с поддержкой интерактивности
Кривой экспорт в PDF и только статические графики/изображения
Нельзя встроить блокнот на сайт/блог
Wolfram Cloud
Тяжелый и тормозной фронтенд, браузер задыхается при рендере даже текстовых ячеек. Нельзя вставить более 3-5 ячеек внутрь блога/сайта как iframe.
Строгая политика к облачным файлам: либо подписка, либо удаление, если отсутствует активность
Ограниченный функционал графики и отображения выражений
Работает исключительно при наличии интернета
Превратится в тыкву при желании WRI (Wolfram Research Inc)
Неужели нельзя сделать все "хорошо". Взглянем на Jupyter Notebook, к примеру. Там нет этих недостатков, весь блокнот уместится в единый HTML файл
Эта портативность и легкость заразительна. Взглянем на Observable, где великолепно решены проблемы с интерактивностью и динамикой, чего очень не хватает в Jupyter
Интересная особенность Observable - любая переменная считается динамической. Это все равно, что если бы в Wolfram Mathematica весь блокнот был внутриDynamicModule
.
Тернистый путь разработки экосистемы
Здесь могла быть просто демонстрация готового проекта. Но вряд ли кто-то поспорит: Хабр - торт, когда можно чему-то научиться после прочтения текста или узнать что-то новое.
Чтобы решить проблему портативности и совместимости, нет никакого другого варианта, как использовать веб-браузер, который гарантирует, что все будет работать предсказуемо на любой платформе или системе.
WebGUI к консоли
У нас есть Wolfram Engine - это консольное приложение, поддерживающее stdin
/stdout
и ничего более, за исключением библиотек работы с файлами, сокетами и парочкой инструментов для OpenCL и CUDA вычислений. Пример Jupyter показал, что HTTP сервер с WebSockets протоколом для быстрого обмена TCP-подобными сообщениями с клиентским приложением работает круто. Есть ли HTTP сервер для Wolfram Language?..
Нет, но его можно всегда написать. Эта героическая задача была решена с нуля @KirillBelovTest. Можете почитать здесь. Причем не только про сервер, но и про высокоскоростной интерфейс сокетов (sockets), написанный им же с нуля на чистом Си для поддержания кроссплатформерности.
Таким образом, задача по прикручиванию веб-интерфеса складывается из достаточно типичных для современного веба блоков
В качестве шаблонизатора я написал WSP (Wolfram Script Pages) как PHP-подобный язык, только для Wolfram Language. Но сейчас он был замещен его наследником WLX (Wolfram Language XML), вдохновленным синтаксисом JSX.
Пример, как это может выглядеть
(* package manager to make sure you will get the right version *)
PacletInstall["JerryI/LPM"];
<< JerryI`LPM`
PacletRepositories[{
Github -> "https://github.com/KirillBelovTest/Objects",
Github -> "https://github.com/KirillBelovTest/Internal",
Github -> "https://github.com/JerryI/CSocketListener",
Github -> "https://github.com/KirillBelovTest/TCPServer",
Github -> "https://github.com/KirillBelovTest/HTTPHandler",
Github -> "https://github.com/KirillBelovTest/WebSocketHandler",
Github -> "https://github.com/JerryI/wl-misc",
Github -> "https://github.com/JerryI/wl-wlx"
}]
(* packages for HTTP server *)
<<KirillBelov`Objects`
<<KirillBelov`Internal`
<<KirillBelov`CSockets`
<<KirillBelov`TCPServer`
<<KirillBelov`HTTPHandler`
<<KirillBelov`HTTPHandler`Extensions`
(* WLX scripts *)
<<JerryI`WLX`
<<JerryI`WLX`Importer`
<<JerryI`WLX`WLJS`
(* setting the directory of the project *)
SetDirectory[If[StringQ[NotebookDirectory[]], NotebookDirectory[], DirectoryName[$InputFileName]]]
Print["Staring HTTP server..."];
tcp = TCPServer[];
tcp["CompleteHandler", "HTTP"] = HTTPPacketQ -> HTTPPacketLength;
tcp["MessageHandler", "HTTP"] = HTTPPacketQ -> http;
(* main app file *)
index := ImportComponent["index.wlx"];
http = HTTPHandler[];
http["MessageHandler", "Index"] = AssocMatchQ[<|"Method" -> "GET"|>] -> Function[x, index[x]]
SocketListen[CSocketOpen["127.0.0.1:8010"], tcp@# &];
StringTemplate["open http://``:``/"][httplistener[[1]]["Host"], httplistener[[1]]["Port"]] // Print;
While[True, Pause[1]];
где в директории, откуда вы запускаете скрипт, находятся два файла
index.wlx
Main = ImportComponent["main.wlx"];
<Main Request={$FirstChild}/>
А также файл с самим "приложением"
main.wlx
(* /* HTML Page */ *)
<html>
<head>
<title>WLX Template</title>
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"/>
</head>
<body>
<div class="min-h-full">
<header class="bg-white shadow">
<div class="flex items-center mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<h1 class="text-3xl font-bold tracking-tight text-gray-900">Title</h1>
</div>
</header>
<main>
<div class="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
Local time <TextString><Now/></TextString>
</div>
</main>
</div>
</body>
</html>
Зайдя на страницу в браузере 127.0.0.1:8010, можно будет увидеть следующее
Как видно, все страницы представляют собой обычные HTML-документы с расширенным синтаксисом, таким образом, что теги, начинающиеся с заглавной буквы, считаются выражениями Wolfram Language
<TextString><Now/></TextString>
Формируя страницы из компонент, можно писать своего рода "веб-приложения". Далее я не буду вдаваться в подробности этого подхода, так как объем материала тянет на отдельную публикацию.
Интерпретатор языка Wolfram в браузере
Зачем? Он же уже есть!
Вернемся к простой задаче, как показать график из консоли, если кто-то не заплатил 300$ WRI. Откроем терминал и введем wolframscript
, затем
Plot[x, {x,0,1}]
и увидим следующее
- Graphics -
На самом деле можно вытащить больше информации, применив
ExportString[Plot[x, {x,0,1}], "ExpressionJSON"]
[
"Graphics",
[
"Line",
[
"List",
[
"List",
2.040816326530612e-8,
2.040816326530612e-8
],
[
"List",
3.0671792055962676e-4,
3.0671792055962676e-4
],
Это ни что иное, как "рецепт" приготовления этого блюда. Остается найти повара, точнее написать. Как и на чем? Кажется очевидным, исходя из факта того, что у нас будет WebGUI
//набор будущих функций
const core = {};
//интерпретатор
const interpretate = (expr, env = {}) => {
if (typeof expr === 'string') return expr; //строка
if (typeof expr === 'number') return expr; //число
//значит это выражение WL
const args = expr.slice(1);
return core[expr[0]](args, env);
}
Окей, теперь давайте объявим выражение List
. Я думаю, следующее будет ясным без дополнительных разъяснений
//async это круто!
core.List = async (args, env) => {
const list = [];
const copy = {...env};
for (const i of args) {
//запишем результат интерпретации списка или массива WL в массив list
//env передается как глубокая копия, для того, чтобы изменения ее внутри не влияли на обзекты снаружи списка
list.push(await interpretate(i, copy));
}
return list;
}
Почему так сложно? Покажу пример использования List
Graphics[{Red, Point[{-0.5,0}], {Green, Point[{0,0}]}, Point[{0.5, 0}]}]
Здесь видно, что {}
или по-другому List[]
изолирует "shared" параметры среды внутри от других листов, которые не являются вложенными. По этой причине в версии JS мы копируем переменную env
, которая будет хранить такие опции, как цвет, толщина, да и все что угодно.
Остается реализовать Line
, RGBColor
, саму функцию Graphics
и мы уже можем строить графики. Полный код приведен здесь, однако я приведу пример на псевдо-языке, как это может выглядеть
core.Line = async (args, env) => {
const data = await interpretate(args, env);
env.canvas.putLine(data, env.color);
return null;
}
core.RGBColor = async (args, env) => {
const color = await interpretate(args, env);
env.color = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
}
В целом, нужно также рассмотреть ситуации, когда вложенные выражения меняются со временем и бегать по соответствующей ветке древа для того, чтобы пересчитать положение линий и т.д., например, для таких случаев
Важно заметить, что ядро Wolfram здесь никак не вовлекается, разве что для непосредственного семплирования функции, находящейся в Plot
. Живую демонстрацию того, что может этот интерпретатор, если наполнить его всеми необходимыми примитивами, можно увидеть на странице с документацией.
Добавив еще пару функций и библиотеку THREE.js, получается делать такие картинки
VectorPlot3D[{x, y, z}, {x, -1, 1}, {y, -1, 1}, {z, -1, 1}, VectorColorFunction -> Function[{x, y, z, vx, vy, vz, n}, ColorData["ThermometerColors"][x]]][[1]];
%/. {RGBColor[r_,g_,b_] :> Sequence[RGBColor[r/50,g/50,b/50], Emissive[RGBColor[r,g,b], 5]],};
Graphics3D[{%, Roughness[0], Sphere[{0,0,0}, 0.9]}, Lighting->None, RTX->True]
Graphics3D[{
Blue, Cylinder[],
Red, Sphere[{0, 0, 2}],
Yellow, Polygon[{{-3, -3, -2}, {-3, 3, -2}, {3, 3, -2}, {3, -3, -2}}]
}]
Как связать Wolfram Kernel и Javascript машину?
Нужен наиболее эффективный способ передачи данных. Кроме того, если это будет интерфейс блокнота, нужен API.
Мне никогда не нравилась идея классических API, которые сейчас имеются для взаимодействия фронтенда с бэкендом у современных приложений. Я испытываю легкое чувство неловкости, объявляя что-то подобное
//где-то на сервере/клиенте
switch(command) {
'ping':
printf('Pong!');
break;
...
}
//где-то на клиенте/сервере
send({command: 'ping', payload: []});
Я, конечно, утрирую, но это точно плохой путь для блокнота Wolfram Language. У нас есть вебсокеты и интерпретатор, верно?
(* сервер *)
serialize = ExportString[#, "ExpressionJSON"]&;
WebSocketSend[client, Alert["Hello world!"] // serialize]
/* клиент */
const Socket = new WebSocket("ws://127.0.0.1:port");
Socket.onmessage = function (event) {
interpretate(JSON.parse(event.data));
};
//какая-то функция нужная на фроентенде
core.Alert = async (args, env) => {
const text = await interpretate(args[0], env);
alert(text);
}
Разве не прелесть? Мы можем разговаривать с UI на том же языке, на котором работает ядро. Очевидно, что если целиться на ячеечную структуру блокнота, пригодятся также и такие функции
FrontEndCreateCell[...]
FrontEndDeleteCell[...]
FrontEndEvaluate[...]
...
Для обратной связи мы можем воспользоваться тем же форматом JSON, так как ничего не стоит отправить данные от JS по каналу веб-сокетов и на стороне Wolfram Kernel сделать подобное
ImportString[input, "JSON"] // HandlerFunction
либо еще проще и быстрее, минуя JSON
input // ToExpression
Многие скажут БЕЗОПАСНОСТЬ, однако, для локального приложения это не вреднее, чем позволять жить у себя NodeJS серверу с, в принципе, неограниченными правами на чтение/запись и запуск любого системного процесса.
Его величие - редактор
Это, вероятно, чуть ли не самое сердце любого блокнотного интерфейса. Самые очевидные функции могут быть получены почти любым JS редактором кода:
подсветка синтаксиса (желательно, любого)
навигация как в привычных редакторах, так и в Vim
скорость и легкость
Однако, вспомним про синтаксический сахар и требование к "редактируемости" выходных ячеек.
Как отобразить график внутри кода?
Декорации - этот концепт был введен еще давно до появления JS и веб-редакторов кода, но в полной мере воплощен только в CodeMirror 6. Представьте себе, что мы можем написать некий виджет, который заменяет собой выражение в виде строки
//выражение, которое ищется и заменяется
const ExecutableMatcher = (ref) => { return new MatchDecorator({
regexp: /FrontEndExecutable\["([^"]+)"\]/g,
decoration: match => Decoration.replace({
widget: new ExecutableWidget(match[1], ref),
})
}) };
//сам виджет
class ExecutableWidget extends WidgetType {
constructor(name, ref) {
super();
this.ref = ref;
this.name = name;
}
eq(other) {
return this.name === other.name;
}
//та самая функция которая заменяет текст на DOM элемент
toDOM() {
let elt = document.createElement("div");
//абстрактно создаем объект и исполняем его
this.fobj = new ExecutableObject(this.name, elt);
this.fobj.execute()
this.ref.push(this.fobj);
return elt;
}
ignoreEvent() {
return true;
}
destroy() {
}
}
Это, так называемые, ReplacingDecorations. Исходный текст под ними остается нетронутым, а декорируемое выражение атомизируется, занимая место лишь одного символа для каретки. В этой связи возникает простая и элегантная идея отображения всех интерактивных объектов как выражение-"ключевая строка" FrontEndExecutable["id"]
с ссылкой на объект JSON, где будет находиться рецепт для интерпретатора, чтобы отобразить красивый график
Остается лишь создать правила, по которым выражения будут заменяться на ключевые строки и передавать параллельно сопутствующие данные в виде JSON.
Не пугайтесь абстрактного кода, позже будет ссылка на CodeSandbox, где эти игры с редактором CodeMirror можно попробовать самим.
Что насчет математических выражений?
Грубо говоря, как отобразить дробь? А дробь в дроби в дроби ... Я полагаю, что лучше один раз показать на примере
и как это можно "закодировать"
CMFraction[1, CMSqrt[6]]
Остается пробежаться регулярными выражениями и распарсить это в редакторе как
Editor
CMFraction
Editor
CMSqrt
Editor
Зачем там написано Editor - я хотел лишь подчеркнуть, что числитель и знаменатель дроби, как и ячейка под корнем, обязаны быть такими же текстовыми редакторами с подсветкой синтаксиса, как и "основной" редактор
Итого на такое выражение потребуется создать 3 инстанса CodeMirror 6. Что не так плохо. А что на счет матриц?
CMGrid[{{1,0,0}, {0,1,0}, {0,0,1}}]
Итого 10 редакторов! Хотите 26? Тогда попробуйте посмотреть результат этого выражения
Table[If[PrimeQ[i], Framed[i, Background->Yellow], i], {i, 1, 100}]
Это скриншот со страницы документации проекта, где это работает вживую
Когда число доходит до 50-100, главный редактор уже значительно тяжелее переваривает изменения в дочерних редакторах.
Я оформил это расширение как отдельный NPM пакет, так люди могут использовать его в своих проектах с Wolfram Language независимо от фронтенда. Ссылка на песочницу.
в песочнице сочетания
Ctrl+-
,Ctrl+/
на выделенном коде создадут индекс и дробь, соотвественно.
Портативность
Так как редактор и ячейки все равно уже "живут" в браузере, значит, экспорт блокнота в HTML файл не составит труда. В предыдущих секциях мы договорились использовать веб-сокеты для управления структурой блокнота, соотвественно, если просто записать последовательность команд при старте блокнота вроде
commands = {
FrontEndCreateCell[...],
FrontEndCreateCell[...],
...
};
и эмулировать это с помощью Javascript при открытии HTML-файла, то эффект будет тот же, что и в настоящем блокноте. Все необходимые библиотеки можно "утащить" туда же.
К примеру, документация к этому проекту сделана подобным образом
Сам факт того, что в браузере "крутится" обрезанная версия интерпретатора Wolfram Language, позволяет переносить часть логики напрямую в браузер. Таким образом, можно сохранить частичную интерактивность, даже без запущенного ядра Wolfram Engine.
Open-source блокнотный интерфейс Wolfram Language
В англоязычной среде и документации он встречается под названием WLJS Frontend. Почему так? Это не так важно.
документация (наполняется)
paypal
Если скомбинировать все методы и подходы, описанные в предыдущих частях, то получится следующее приложение
Особенность в том, что это всего лишь веб-сервер. А само приложение - это страница HTML с самым ванильным Javascript (за исключением библиотек, необходимых для отрисовки графики). Таким образом
пользователь может изменять стиль всего интерфейса;
ядро может произвольно менять структуру документа, а также вызывать любой Javascript код на ней (привет
eval()
);фронтенд доступен с любого устройства, способного открывать заглавную страницу Хабра;
можно экспортировать блокнот в HTML с частичным сохранением интерактивности;
оно принадлежит Вам целиком, не нуждается в интернете и работает локально.
Разумеется для удобства есть версия, где оно обернуто в ElectronJS, что позволяет привнести привычные для системных приложений доступы к проводнику и полноценному контекстному, а также оконному меню.
Ячейки
Зачем меня принуждают писать на Wolfram Language, когда я хочу сделать красивую диаграмму. Мне вообще-то нравится Mermaid
Идея обращения к анонимному файлу .mermaid
мне кажется красивой. Давайте также обратимся к Markdown
.md
# Hey, how was your day?
I think it was fine. It is <?wsp TextString[Now] ?> and I am still writting my post for Habr
Нет, мне вообще на самом деле нравится Tailwind, и я хочу оформлять свои данные с помощью него
.html
<link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet"/>
.html
<ul role="list" class="divide-y divide-gray-100">
<?wsp Table[ ?>
<li class="bg-white shadow my-1">
<span class="flex justify-between round gap-x-6 px-3 py-5 hover:bg-sky-100">
<div class="flex gap-x-4">
<div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900"><?wsp RandomWord[] ?></p>
<p class="mt-1 truncate text-xs leading-5 text-gray-500"><?wsp RandomWord[] ?></p>
</div>
</div>
</span>
</li>
<?wsp , {i,10}] ?>
</ul>
Да зачем мне все эти сложности, я хотел график построить, но если бы можно было его еще покрутить...
Нет, я на самом деле хотел записать в файл
filename.txt
Hello World
JS Cells
Сильной стороной являются ячейки типа .js
, так как сам фронтенд написан в основном на JS. Как я уже описал выше, на сервере и на клиенте работают интерпретаторы Wolfram Language, соотвественно, подписываться на события друг друга или вызывать функции можно напрямую
.js
const element = document.createElement('span');
core.ShowText = (args, env) => {
element.innerText = await interpretate(args[0], env);
}
return element;
и затем из ячейки WL
ShowText["This is a text"] // FrontSubmit
Если пойти дальше, можно делать вещи чуть более сложные
С помощью расширения wljs-esm-support, можно также подключить Node и бандлер ESBuild, тогда у вас появится возможность использовать любой пакет с NPM. Как и сделал я, когда мне понадобилось подключить свой контроллер Nintendo Pro.
LLM Chatbook
Для каждой задачи подойдет свой язык - это бесспорно, но еще лучше, если эту задачу решат за тебя
.llm
Plot a butterfly curve using Wolfram Language
Это дополнение было разработано @KirillBelovTest, который также является автором сервера и сейчас также активно принимает участие в разработке.
Благодаря тому, что llm имеет доступ ко всем ячейкам, и его вывод ничем не отличается от пользовательских ячеек, складывается приятное иммерсивное ощущение, что это не чат-бот, а некий гик, который случайно забежал и набрал что-то с клавиатуры в блокноте.
Редактор
Как и было описано ранее, он поддерживает математический ввод и синтаксический мёд в полной мере благодаря CodeMirror 6
Вопрос, как сделать autocomplete для тех символов, которые объявил пользователь? Как оказалось, с 1999 года в Wolfram Kernel есть следующая функция
$NewSymbol = Print["Name: ", #1, " Context: ", #2] &
Таким образом, можно буквально отслеживать все, что было создано за текущую сессию, и отправлять эти данные в браузер.
Динамика и интерактивность
Разумеется, что нельзя соревноваться с Mathematica, не имея в арсенале инструментов для создания динамических графиков и ползунков.
В процессе создания я значительно переработал этот концепт. Меня раздражала непредсказуемость поведения динамических выражений в Mathematica, которые рано или поздно приводили к падению всего приложения.
Зачем пересчитывать все заново, когда поменялись данные, если можно делать это селективно
core.Line = () => {
//получаем все данные
//обрабатываем
//рисуем
canvas.putLine();
}
core.Line.update = () => {
//обновляем
canvas.updateLine();
}
Я к тому, что у каждой функции должен быть метод для обновления, если данные поменялись. И на каждом выражении можно принять решение о том, как и что пересчитать.
Минусом такого подхода является пожалуй то, что этот метод нужно писать вручную для каждого "важного" для пользователя выражения (в основном графические примитивы), как в Mathematica по-умолчанию интерпретатор проходится по всему древу одинаково, что при первом запуске, что при обновлении данных.
Следующее изменением - событийно-ориентированный подход. Возьмем слайдер
slider = InputRange[-1,1,0.1, "Label"->"Length"]
и привяжем к нему функцию-обработчик
EventHandler[slider, Function[l, length = l]];
EventFire[slider, 0]; (* шарахнем один раз, чтобы все инициализировалось *)
А теперь сам элемент, который будет под контролем
Graphics[{Cyan,
Rectangle[{ -1,1 }, {length // Offload, -1}]
}]
Очевидно, это сразу больше кода, однако, хирургическая точность таких методов позволяет эффективно обновлять данные
Такую отзывчивость сложно представить в Mathematica. Либо такой пример
Обладателей Nvidia RTX приглашаю взглянуть на эти две сферы
Graphics3D[{
{Emissive[Red], Sphere[{0,0,2}]},
{White, Sphere[]}
}, Lighting->None, RTX->True]
Расширяемость
Разумеется, имеется система плагинов/расширений, где возможно добавить новые типы ячеек, влиять на ход исполнения ячеек, расширять библиотеку функций и т.п. Сам проект собран из более 10 расширений, половина из которых являются системными и могут работать отдельно.
Хороший пример - анимация на сайте конференции Wolfram Saint-Petersburg 2023, где используются всего лишь два компонента:
wljs-interpreter - интерперататор WL
wljs-graphics-d3 - библиотека реализующая примитивы
Graphics
Слайды/Презентация из компонентов
Работая в академической среде, мне никогда не нравилось готовить презентации к докладам, на визуальное исполнение которых уходит большая часть времени, вместо самого содержания. Почему так? Поясню:
для обработки данных используется среда A
для визуализации среда Б
для слайдов среда С
Передача данных между ними осуществляется путем сериализации в файл, что, скажем, не очень быстро и удобно. Ах да, не забудем про
перетаскивание блоков с информацией / копирование их на другие слайды
В open-source сообществе уже есть решения на этот счет, скажем, - RevealJS с возможностью писать слайды на языке Markdown. Однако, здесь все равно не хватает компонент и, как собственно, способа передачи графических данных.
Markdown поддерживает HTML из коробки, значит, у нас уже есть доступ к стилям и оформлению, если хочется. Допустим, как сделать две колонки?
.html
<div>
<div style="width:50%; float:left" >1</div>
<div style="width:50%; float:right">2</div>
</div>
Было бы здорово сделать такой компонент. С использованием WLX это возможно
.wlx
Columns[C1_, C2_] := With[{SR = If[NumberQ[Ratio], 100.0 Ratio, 50]},
<div>
<div style="width: {SR}%; float:left;">
<C1/>
</div>
<div style="width: {100-SR}%; float:right;">
<C2/>
</div>
</div>
]
Теперь вернемся к нашим слайдам, мы ведь с этого начали
.slide
# Title
<Columns>
<Identity>
First column
</Identity>
Second one
</Columns>
Не обращайте внимание на Identity
оператор, так как он нужен чтобы подсказать WL, что вторая фраза - это уже второй аргумент к функции Columns
.
А что насчет графиков?
Plt3D = Graphics3D[Cuboid[]];
.slide
# Embed some figures
Even 3D
<div style="text-align: center; display: inline-flex;">
<Plt3D/>
</div>
Try to move it using your mouse
Можно привязаться к событиям: появление фрагмента на слайде или его смена, либо вставлять напрямую компоненты ввода (ползунки) и кнопки. Ниже представлена презентация, которую я использовал на докладах в 2023 году
Она, как и все другие примеры, доступна в самом приложении фронтенда через оконное меню File - Open Examples.
Приложения на WLX
Здесь пример, как можно использовать динамику и язык разметки WLX, чтобы набросать простенькую утилиту с графическим интерфейсом для распознавания таблиц с картинок
.wlx
LeakyModule[{img, output1, output2, Ev, pipe, EditorRaw, EditorProcessed},
(* поле drop file *)
Ev = InputFile["Drop an image here"];
(* вешаем на него обработчик *)
EventHandler[Ev, Function[file,
(* импорт по формату и само распознование текста *)
pipe = ImportByteArray[file["data"]//BaseDecode, FileExtension[file["name"]]//ToUpperCase];
pipe = Binarize[pipe];
pipe = TextRecognize[pipe];
output1 = ToString[pipe, InputForm];
output2 = ToString[(ToExpression /@ StringSplit[#, "
"]) &/@ StringSplit[pipe, "
"], InputForm];
]];
(* выходные значения *)
output1 = "- none -";
output2 = "- none -";
(* два поля вывода с подсветкой синтаксиса *)
EditorRaw = EditorView[output1 // Offload] // CreateFrontEndObject;
EditorProcessed = EditorView[output2 // Offload] // CreateFrontEndObject;
(* шаблон разметки выходной ячейки в HTML (WLX) *)
<div>
<div style="display: flex;"><Ev/></div>
<p>Raw string</p>
<div style="margin-top: 0.5em; margin-bottom: 0.5em; display: flex; border: 2px dashed skyblue;"><EditorRaw/></div>
<p>Processed string</p>
<div style="margin-top: 0.5em; margin-bottom: 0.5em; display: flex; border: 2px dashed deepskyblue;"><EditorProcessed/></div>
</div>
]
Видео в действии
Вывод ячейки в окно
Это побочная функция, созданная для возможности показывать слайды. Однако, также может быть полезной, когда блокнот становится слишком большим для навигации. Мой рабочий стол обычно выглядит как-то так
Ограничения
Обойти достижения WRI последних 20-лет двум разработчикам за год невозможно и бессмысленно (у нас нет такой цели и не будет). WLJS Frontend это альтернативный инструмент со своими преимуществами и недостатками, где для решения архитектурных проблем в одних областях были приняты компромиссные решения в других, но не замена.
@KirillBelovTestи я постарались скомпилировать бинарные файлы компонент веб-сервера под каждую платформу, однако, различия все же встречаются, что периодически пополняет банк Issues на гитхабе. Если нужна "горячая поддержка", вступайте в группу поддержки в Телеграмме.
Из других примеров, до сих пор нет функции Circle
в пакете Graphics, просто потому, что она редко используется в типичных plot-функциях Mathematica и чьи-то руки не дошли до того, чтобы написать десяток строчек кода на JS. Тем не менее, большая часть функций, которая касается построения данных по точкам, уже покрыта - смотрите здесь.
Проект развивается и дополняется почти каждый день. Это не готовый продукт, в отличие от Wolfram Mathematica.
"Этот список" из цитаты в начале статьи
Вызов брошен, а как же ответ? Вот адаптированные сниппеты из списка, который показал автор
d=theta@t-phi@t;
sol = NDSolve[{#''@t==-#4#2''[t]Cos@d-##3#2'@t^2Sin@d-Sin@#@t&@@@{{theta,phi,1,.5},{phi,theta,-1,1}},theta@0==2,phi@0==1,theta'@t==phi'@t==0/.t->0},{theta,phi},{t,0,60}];
With[{h = {Sin@#@#2,-Cos@#@#2}&},
With[{f = theta~h~u+phi~h~u /. First[sol], m1 = theta~h~u /. First[sol]},
LeakyModule[{points, time = 0., handler, task},
EventHandler[EvaluationCell[], {"destroy"->Function[Null, TaskRemove[task]]}];
handler := (points = Table[f, {u, 0., time,0.1}]; pendulum1 = Table[m1, {u, {time}}] // First; pendulum2 = points // Last;);
handler;
task = SetInterval[
time = time + 0.1;
handler;
If[time > 59., TaskRemove[task]];
, 70];
Graphics[{
Line[points // Offload], PointSize[0.05], Red,
Point[pendulum1 // Offload],
Point[pendulum2 // Offload],
Line[{pendulum1 // Offload, pendulum2 // Offload}]
}, Controls->True, Axes->True, TransitionDuration->10, TransitionType->"Linear"]
]
]
]
и демонстрация, если это запустить в блокноте
Другой "сниппет" из той же ветки
StreamPlot[{x^2,y},{x,0,3},{y,0,3}]
LeakyModule[{data, frame, i = 1},
data = CellularAutomaton[{224,{2,{{2,2,2},{2,1,2},{2,2,2}}},{1,1}}, Table[RandomInteger[{0,1}], {x,200}, {y,400}], 50];
frame = 255 data[[i]];
EventHandler[EvaluationCell[], {"destroy"->Function[Null, TaskRemove[task]]}];
task = SetInterval[
i = i + 1;
frame = 255 data[[i]];
If[i > 49,
data = CellularAutomaton[{224,{2,{{2,2,2},{2,1,2},{2,2,2}}},{1,1}}, data//Last, 50];
i = 0;
];
, 50];
Image[frame // Offload]
]
SphericalPlot3D[Re[Sin[\[Theta]]Cos[\[Theta]]Exp[2I*\[CurlyPhi]]],{\[Theta],0,\[Pi]},{\[CurlyPhi],0,2\[Pi]}]
Спасибо за внимание ?
UPD: Грамматика