Open-source блокнот Wolfram Language или на как воссоздать минимальное ядро Mathematica на Javascript и не только

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

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

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

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

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

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

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

TLDR Якорь

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

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

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

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

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

Wolfram Blog. 30y anniversary
Wolfram Blog. 30y anniversary
Jupyter Notebook
Jupyter Notebook

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

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

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

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

Важно отделять 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
Пример синтаксического сахара, дошедшего до 3D графики
Пример синтаксического сахара, дошедшего до 3D графики

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

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

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

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

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

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

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

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

1/Sqrt[2]
\frac{1}{\sqrt{2}}

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

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

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

  • Wolfram Mathematica

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

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

    • Клиент и среда связаны, нельзя подключаться с телефона / тостера

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

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

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

    • Сложно (неочевидно) как добавить другие языки или типы ячеек на низком уровне (нативно)

  • Wolfram Cloud

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

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

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

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

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

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

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

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

Curve Fitting. Встроенный набор слайдеров, форм ввода на все случаи жизни. Любая переменная считается динамической
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
Страничка, полученная с помощью веб-сервера на основе 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
Пример динамического графика (и) с использованием интерпретатора 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
Космический 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
Стандартный пример 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 можно попробовать самим.

Что насчет математических выражений?

Грубо говоря, как отобразить дробь? А дробь в дроби в дроби ... Я полагаю, что лучше один раз показать на примере

\frac{1}{\sqrt{2}}

и как это можно "закодировать"

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
Пример 26 запущенных инстансов редактора кода CodeMirror 6

Когда число доходит до 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

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

Типичный вид блокнотного интерфейса WLJS Frontend для меня
Типичный вид блокнотного интерфейса 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 ячейки
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 с шаблонизатором
Результат выполнения ячеек типа HTML с шаблонизатором

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

Стандартные средства Mathematica так не умеют к сожалению.
Стандартные средства 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
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, где используются всего лишь два компонента

  • 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]}] 
Типичный трехмерный график
Типичный трехмерный график

Спасибо, за ваше внимание

Источник: https://habr.com/ru/articles/767490/


Интересные статьи

Интересные статьи

Сравниваем традиционный способ извлечения значений и деструктуризацию (ES6) в JavaScriptВ этой статье мы рассмотрим традиционное присваивание значений из объектов в переменные и новый синтаксис дестру...
В попытке найти годную статью по настройке уведомлений в браузере, я получал только статьи где в основном описывалось использование совместно с Firebase, но мне такой вар...
Эта статья представляет собой углублённое введение в итерируемые объекты (iterables) и итераторы (iterators) в JavaScript. Моя главная мотивация к её написанию заключалась в подготовке ...
Привет, Хабр! Представляю вашему вниманию перевод статьи «f5 Reasons AI Won’t Replace Humans… It Will Make Us Superhuman». Многие говорят, что ИИ с немыслимой скоростью забирает у нас работу. ...
Вчера компания Intel представила публике свой новый восьмиядерный процессор (16 потоков) i9-9900KS с тактовой частотой в 5,0 GHz на каждое ядро в режиме Turbo. Режим Turbo в процессорах Intel — э...