На Хабре уже проскакивали упоминания о совместимых или систем-копиях Wolfram Mathematica, но реализованных на других языках, как, скажем, Mathics. Автор статьи @ITSummaупомянул в самом начале

На Mathics такое не получится, как и многие другие примеры из этого списка тоже не сработают. Вообще, для Wolfram Language (WL) практически невозможно создать полноценный интерпретатор с открытым исходным кодом, потому что многие встроенные решатели являются проприетарной собственностью компании Вольфрама. Однако попытаться можно. 

Сложно поспорить с этим утверждением, однако, возможен компромиссный вариант, позволяющий использовать все те же "решатели", но с немного иной open-source оберткой снаружи. В качестве ответа я представляю систему, которая не только воспроизводит многие ключевые функции блокнота Mathematica с нуля, но и расширяет функционал гораздо дальше, чем там, где очертил его границы Стивен Вольфрам, создав эту потрясающую систему более 30-ти лет назад.

::: Это не готовый продукт и не замена Wolfram Mathematica

Вставка для привлечения внимания

Бороздящий просторы корабль, процедурно сгенерированный WL. К сожалению, RTX у меня на интегрированной Intel UHD не очень хорош в браузере.

Здесь мне потребовалось завлечь пользователей этим замечательным корабликом. То, на чем он написан, - это ни что иное как Wolfram Language и то, где он исполняется здесь и сейчас, - Ваш браузер (для тех, кто кликнул на картинку).

TLDR Якорь

страница проекта

документация (наполняется)

Но я обманул Вас, это не open-source блокнот, а нечто другое, но не менее важное, о чем Вы узнаете позже в секциях ниже. Начнем с привычных разделов...

Блокнот? Швейцарский нож

Код, иллюстрации, data-science, презентации - все сегодня возможно написать в пределах скевоморфиозного вида интерфейсов - блокнота

Wolfram Blog. 30y anniversary
Jupyter Notebook

Ячейки разного типа это безусловно преимущество, особенно это касается типа Markdown, когда его "привезут" в Wolfram Mathematica - неизвестно.

Безымянный проект, о котором сегодня пойдет речь. Подготовка слайдов в смешанном режиме Markdown/Wolfram

Про презентации мы еще поговорим позже.

Бесплатный сыр

Важно отделять Wolfram Language от того, что его реализует - Wolfram Mathematica. Однако это также не совсем верно, так как Wolfram Mathematica это язык и интерфейс к нему (фронтенд), которые, вероятно, соразмерны друг с другом.

(Около-)Свободная реализация языка (см. комментарии про ограничения)со стандартными библиотеками уже давно доступна - это Wolfram Engine, который подобно Питону можно подключить в качестве скриптового языка к чему-угодно.

Отличия интерфейса блокнотов Mathematica от других

Некоторые из вас могут посчитать следующие пункты полезными или бесполезными конкретно для вашей работы или подхода к программированию, однако, нельзя опускать сам факт их реализации - это технически и концептуально сложная задача, которая была великолепно решена. Такое нельзя найти в Jupyter, Obsevable (d3-express), VSCode Notebook API.

Синтаксический сахар

Возведем идею формочек с цветом, которые многие видят в Visual Studio Code, редактируя какие-нибудь CSS цвета

Хлеб с маслом фронтендера

в бесконечность и получим

Graphics3D[Sphere[]]
% /. Sphere -> Cuboid
Пример синтаксического сахара, дошедшего до 3D-графики

Здесь основная идея состоит в том, что сам график с точки зрения среды - это набор выражений и символов. Когда он рисуется на экране - это все еще тот же набор символов и выражений, с которым можно взаимодействовать. А трехмерный куб - это просто одна из возможных интерпретаций.

Выходные ячейки - редактируемы

Я не знаю почему, но почти все блокнотные интерфейсы просто игнорируют эту опцию

ObservableHQ. Выходная ячейка только для просмотра
Jupyter Notebook. Ну хоть что-то оно вывело, уже плюс.

Мы получили результат - это тоже выражение. Почему бы не использовать его в последующих вычислениях?

Возможно, языков программирования, которые могли бы воспользоваться этим на благо просто мало.

Двумерный математический ввод

Здесь я явно предвзят, так как являюсь физиком-теоретиком. Что вам нравится больше?

1/Sqrt[2]

В редакторе Хабра это сложно показать, но возможность миксовать код и математические выражения подобные тем, что в LaTeX, - это потрясающе. Представьте, если обобщить это, писать код, вставлять изображения или другие интерактивные объекты в то время, как редактор будет это видеть и обрабатывать как все тот же код. Очевидно, это работа для регулярных выражений.

Зачем изобретать велосипед с открытым исходным кодом

Очевидный вопрос, ведь рынок уже удовлетворен тем, что создает WRI. Время привести недостатки

  • Wolfram Mathematica

    • Проприетарный формат/среда, который/ая стоит дорого

    • Тяжелый интерфейс (в плане отзывчивости), нестабильный UI (краш, фриз это обычное дело)

    • Клиент и среда связаны, сложно вести удаленную сессию с телефона/тостера

    • Нельзя делиться блокнотами с поддержкой интерактивности

    • Кривой экспорт в PDF и только статические графики/изображения

    • Нельзя встроить блокнот на сайт/блог

  • Wolfram Cloud

    • Тяжелый и тормозной фронтенд, браузер задыхается при рендере даже текстовых ячеек. Нельзя вставить более 3-5 ячеек внутрь блога/сайта как iframe.

    • Строгая политика к облачным файлам: либо подписка, либо удаление, если отсутствует активность

    • Ограниченный функционал графики и отображения выражений

    • Работает исключительно при наличии интернета

    • Превратится в тыкву при желании WRI (Wolfram Research Inc)

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

HTML версия блокнота Jupyter. Изображение позаимствовано, так как последнее обновление JupyterLab сломало функцию экспорта ;)

Эта портативность и легкость заразительна. Взглянем на Observable, где великолепно решены проблемы с интерактивностью и динамикой, чего очень не хватает в Jupyter

Curve Fitting. Встроенный набор слайдеров, форм ввода на все случаи жизни.

Интересная особенность 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, можно будет увидеть следующее

Страничка, полученная с помощью веб-сервера на основе Wolfram Engine

Как видно, все страницы представляют собой обычные 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]})`;
}
То, как будет выглядеть изображение графика, если выполнить все необходимые пункты

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

Пример динамического(ой) графика(ки) с использованием интерпретатора WLJS

Важно заметить, что ядро 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]
Космический VectorPlot
Graphics3D[{
  Blue, Cylinder[], 
  Red, Sphere[{0, 0, 2}],
  Yellow, Polygon[{{-3, -3, -2}, {-3, 3, -2}, {3, 3, -2}, {3, -3, -2}}]
}]
Стандартный пример 3D-графики Mathematica, исполненный в WLJS

Как связать 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}]

Это скриншот со страницы документации проекта, где это работает вживую

Пример 26 запущенных инстансов редактора кода CodeMirror 6

Когда число доходит до 50-100, главный редактор уже значительно тяжелее переваривает изменения в дочерних редакторах.

Я оформил это расширение как отдельный NPM пакет, так люди могут использовать его в своих проектах с Wolfram Language независимо от фронтенда. Ссылка на песочницу.

в песочнице сочетания Ctrl+-, Ctrl+/ на выделенном коде создадут индекс и дробь, соотвественно.

Портативность

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

commands = {
  FrontEndCreateCell[...],
  FrontEndCreateCell[...],
  ...
};

и эмулировать это с помощью Javascript при открытии HTML-файла, то эффект будет тот же, что и в настоящем блокноте. Все необходимые библиотеки можно "утащить" туда же.

К примеру, документация к этому проекту сделана подобным образом

Псевдоживые ячейки в документации

Сам факт того, что в браузере "крутится" обрезанная версия интерпретатора Wolfram Language, позволяет переносить часть логики напрямую в браузер. Таким образом, можно сохранить частичную интерактивность, даже без запущенного ядра Wolfram Engine.

Open-source блокнотный интерфейс Wolfram Language

В англоязычной среде и документации он встречается под названием WLJS Frontend. Почему так? Это не так важно.

страница проекта

документация (наполняется)

paypal

Если скомбинировать все методы и подходы, описанные в предыдущих частях, то получится следующее приложение

Типичный вид блокнотного интерфейса WLJS Frontend для меня

Особенность в том, что это всего лишь веб-сервер. А само приложение - это страница 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
Markdown ячейки

Нет, мне вообще на самом деле нравится 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>
Результат выполнения ячеек типа HTML с шаблонизатором

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

Стандартные средства Mathematica так не умеют, к сожалению.

Нет, я на самом деле хотел записать в файл

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
Chatbook

Это дополнение было разработано @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, где используются всего лишь два компонента:

Слайды/Презентация из компонентов

Работая в академической среде, мне никогда не нравилось готовить презентации к докладам, на визуальное исполнение которых уходит большая часть времени, вместо самого содержания. Почему так? Поясню:

  • для обработки данных используется среда 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: Грамматика