На Хабре уже проскакивали упоминания о совместимых или систем-копий Wolfram Mathematica, но реализованных на других языка, как, скажем, Mathics. Автор статьи @ITSummaупомянул в самом начале
На Mathics такое не получится, как и многие другие примеры из этого списка тоже не сработают. Вообще, для Wolfram Language (WL) практически невозможно создать полноценный интерпретатор с открытым исходным кодом, потому что многие встроенные решатели являются проприетарной собственностью компании Вольфрама. Однако попытаться можно.
Сложно поспорить с этим утверждением, однако, возможен компромиссный вариант, позволяющий использовать все те же "решатели", но с немного иной open-source оберткой снаружи. В качестве ответа я представляю систему, которая не только воспроизводит многие ключевые функции блокнота Mathematica с нуля, но и расширяет функционал гораздо дальше, чем где очертил его границы Стивен Вольфрам, создав эту потрясающую систему более 20-ти лет назад.
::: Это не готовый продукт и не замена Wolfram Mathematica
Вставка для привлечения внимания
Здесь мне потребовалось завлечь пользователей этим замечательным корабликом. То на чем он написан - это ни что иное как Wolfram Language и то, где он исполняется здесь и сейчас - Ваш браузер (для тех, кто кликнул на картинку).
TLDR Якорь
страница проекта
документация (наполняется)
Но я обманул Вас, это не open-source блокнот, а нечто другое, но не менее важное, о чем Вы узнаете позже в секциях ниже. Начнем с привычных разделов...
Блокнот? Швейцарский нож
Код, иллюстрации, data-science, презентации - все сегодня возможно написать в пределах скевоморфиозного вида интерфейсов - блокнота
Ячейки разного типа это безусловно преимущество, особенно это касается типа Markdown, когда его "привезут" в Wolfram Mathematica - неизвестно.
Про презентации мы еще поговорим позже.
Бесплатный сыр
Важно отделять Wolfram Language от того, что его реализует - Wolfram Mathematica. Однако это также не совсем верно, так как Wolfram Mathematica это язык + интерфейс к нему (фронтенд), который вероятно сравним по размеру с первым.
Свободная реализация языка со стандартными библиотеками уже давно доступна - это Wolfram Engine, который подобно Питону можно подключить в качестве скриптового языка в чему-угодно.
Отличия интерфейса блокнотов Mathematica от других
Mathematica же это реализация + интерфейс, т.е. фронтенд (frontend), которого в открытом доступе нет и не будет.
Некоторые из вам могут счесть следующие пункты полезными или бесполезными конкретно для вашей работы или подхода к программированию, однако, нельзя опускать сам факт их реализации - это технически и концептуально сложная задача, которая была великолепно решена. Такое нельзя найти в 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 Institute)
Неужели нельзя сделать все "хорошо". Взглянем на Jupiter 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+2
,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]}]
Спасибо, за ваше внимание