Занимательное дело — создавать образовательные модели. Приятно видеть, что человек понял какой-то сложный принцип в естественных науках или алгоритмах, взаимодействуя с твоей программой. По профессии я биофизик, так что с уравнениями и математикой обычно у меня нет проблем, а вот с инструментарием для создания наглядных интерактивных моделей — прошел достаточно длинный путь. Начинал делать модели в Matlab, там отличные библиотеки для решения уравнений, есть возможность легко строить графики. Недостаток в том, что результатом сложно поделиться, и достаточно сложно сделать что-то выходящее за рамки, предусмотренные разработчиками. Нужно больше свободы. Также пробовал использовать технологию Flash, в те годы технология была ещё актуальна для web, и язык ActionScript позволял делать достаточно занятые интерактивные модели. Однако сам язык программирования ActionScript не соответствовал моим строгим представлениям о гармонии и порядке, а потом и вовсе технология Flash была вытеснена из браузеров новым стандартом HTML5... К тому времени я уже активно программировал модели в среде BlackBox Component Builder. Это швейцарская open-source разработка, достаточно герметично изолированная от операционной системы IDE, которую они разработали на базе операционной системы ETHOS. Графические библиотеки меня вполне устроили, производительность компилятора, скорость выполнения расчётного кода — тоже. И главное, язык Оберон идеально лёг на моё представление о том, сколько вообще язык программирования должен занимать в голове у специалиста предметной области. Не нужны были какие-то лингвистические причуды, было приятно находиться в зоне комфорта и думать о задаче. Однако в 21-веке распространять компилированные приложения, чтобы показать студентам что-то, или просто выкладывать в Интернете модели — очень сложно. Ведь люди просто боятся запускать сторонние приложения, и антивирусы часто дают ложно-положительные срабатывания. Десктоп приложения для науки и производства — отлично, интерактивные модели для просвещения людей — нет.

В 2014 году вместе с проектом Информатика-21 мы проводили IT-конференцию в Москве. Туда приехали специалисты по Оберон-системам со всей России и даже из Белоруссии. Я тогда не поленился взять свою старенькую камеру, и записал большую часть докладов. Из доклада Алексея Веселовского я узнал про транслятор OberonJS. Тезис доклада простой: JavaScript — это субстанция весьма аморфная, так что разрабатывать на нём что-то крупное — головная боль, а вот если JavaScript оформить Обероном, будет существенная экономия на отладке. Оберон в некотором роде среднее звено между блочным программным конструктором LEGO MINDSTORMS и безграничной свободой Си. В общем, сделать можно на нём что угодно, и при этом вероятность «выстрелить себе в ногу» минимальна. Мужики тогда сделали демку, чтобы показать, как это работает. Код из окошка CodeMirror прямо в клиентском браузере транслируется из Оберона в JavaScript и выполняется. Это бы меня не зацепило, если бы Алексей не сделал демонстрацию, как это можно объединить с фреймворком ProcessingJS!

Строгий простой язык + продвинутая графика в HTML5 = идеальная формула для образования, чтобы пользователи не только видели интерактивную графическую модель, но и могли легко понять алгоритмы в её основе. Люблю приводить цитату Сергея Залмановича Свердлова:

«Оберон — идеальный язык для изучения программирования. Он прост, понятен, неизбыточен. При этом содержит необходимые и достаточные средства структурного, объектно-ориентированного и модульно-компонентного программирования. Оберон великолепно подходит и для изучения методов трансляции, и как объект и как инструмент».

Модуль «Привет Мир!» на Обероне будет выглядеть следующим образом:

MODULE HelloWorld;
IMPORT Log;
BEGIN
 Log.String("Привет Мир!"); Log.Ln
END HelloWorld.	

Синтаксис Оберона очень напоминает язык Паскаль, однако не стоит их путать.

Взяв за основу транслятор OberonJS, я начал экспериментировать, как можно сохранять модули в базе данных, как это отобразить в интерфейсе на сайте. Идея такая, чтобы возможно было хранить код на Обероне в базе данных, загружать в CodeMirror у клиента в браузере, транслировать онлайн и выполнять. Как выглядит интерфейс, который у меня получился, вы можете посмотреть на примере модели самоорганизующейся карты Кохонена. Автор транслятора Владислав Фольц помог сделать систему русификации сообщений об ошибках в коде и вывод точной позиции. В итоге удалось объединить систему хранения модулей с системой учётных записей пользователей и довести сайт до состояния MVP, чтобы получить поддержку моего университета для развития данного гуманитарного проекта.

Набор инструментов для создания моделей свелся к шести модулям: Log для вывода текстовой информации из программы, Math для математических функций, Strings для работы со строками, Draw для рисования, Forms для создания элементов управления и Plot для создания графиков. Последние два модуля пока достаточно сырые, работа над ними продолжается.

Вместо библиотек ProcessingJS, поддержка которых была остановлена, теперь используется библиотека p5.js. Мы оборачиваем вызовы к библиотеке и JavaScript коды в процедуру JS.do, а в остальном — используем синтаксис Оберона. Так как в Обероне запрещено автоматическое преобразование типов, то между REAL и INTEGER преобразование осуществляется процедурами FLOOR и FLT. Чтобы уменьшить вызов явных преобразований, для рисования используется два типа процедур: первые с действительными аргументами, вторые с целыми. При этом, чтобы избежать проблем с размытием линий, для целочисленных вызовов к координатам автоматически добавляется половина пикселя:

MODULE Draw;
IMPORT JS;
...
PROCEDURE Line*(x0, y0, x1, y1: REAL);
BEGIN JS.do("Instance.line(x0,y0,x1,y1)")
END Line;

PROCEDURE LineInt*(x0, y0, x1, y1: INTEGER);
BEGIN JS.do("Instance.line(x0+0.5,y0+0.5,x1+0.5,y1+0.5);")
END LineInt;
...
END Draw.

Звездочка * обозначает экспорт процедуры за пределы области видимости модуля. Instance создаётся в момент запуска модуля через процедуру Draw.Start, и необходимые нам процедуры присваиваются, заменяя стандартные колбэки библиотеки p5.js, так как показано на примере процедуры InnerDraw ниже.

MODULE Draw;

TYPE
 ProcessingType* = POINTER TO RECORD END;

VAR
 Instance: ProcessingType; focus, started: BOOLEAN;

...

PROCEDURE InnerDraw;
BEGIN
 IF DrawProc # NIL THEN
  TrackMouse;
  DrawProc;
  IF FormDraw # NIL THEN FormDraw END
 END
END InnerDraw;

PROCEDURE Start*;
BEGIN
 ASSERT(~started);
 JS.do("let sketchProc = function(p){
 p.preload=Preload;
 p.draw=InnerDraw; p.setup=InnerSetup;
 p.keyPressed=InnerKeyPressed; p.keyTyped=InnerKeyTyped;
 p.mousePressed=InnerPressed; p.mouseReleased=InnerReleased;
 p.mouseOver=InnerOver; p.mouseOut=InnerOut;
 Instance=p;
 }");
 JS.do("var processingInstance = new p5(sketchProc);");
 JS.do("Instance.colorMode(Instance.RGB, 255, 255, 255, 1);");
 JS.do("removeSketch = function() { Remove(); }");
 focus := FALSE;
 started := TRUE
END Start;

END Draw.

Такой подход позволяет полностью изолировать вызовы JavaScript кода при создании интерактивных моделей, так что пользователь работает только с шестью модулями, описанными выше. Когда программа готова, её возможно опубликовать. Если открыть исходный код опубликованной страницы, то мы увидим работу транслятора:

...
var Init = function (Log){

function Do(){
	Log.String("Привет Мир!");
	Log.Ln();
}
Do();
}(Log);

Для дополнительной безопасности транслятор добавляет проверку выхода за границы массивов, что не предусмотрено в JavaScript. К примеру для определения длины строки по дубовым требованиям:

MODULE Strings;

IMPORT JS;

PROCEDURE Length* (s: ARRAY OF CHAR): INTEGER;
VAR i: INTEGER;
BEGIN i := 0;
 WHILE (i < LEN(s)) & (s[i] > 0X) DO INC(i) END
 RETURN i
END Length;

...

получим результат трансляции:

var Strings = function (JS){

function Length(s/*ARRAY OF CHAR*/){
	var i = 0;
	i = 0;
	while (true){
		if (i < s.length && RTL$.charAt(s, i) > 0){
			++i;
		} else break;
	}
	return i;
}

Проверка charAt осуществляется в специальном наборе функций рантайма:

var RTL$ = {
    charAt: function(s, index){
        if (index >= 0 && index < s.length)
            return s.charCodeAt(index);
        throw new Error("index out of bounds: " + index);
    },
...
}

Поскольку такая проверка обходится достаточно дорого по вычислительным ресурсам, то приходится предусмотреть возможность отключения проверки для программ, интенсивно использующих массивы. Наглядно разница в скорости выполнения видна в программе сортировки вставками. В настоящий момент — это главная проблема проекта. Уверен, что среди читателей, найдётся немало специалистов по JavaScript. Может быть кто-то подскажет, как сделать проверку выхода за границы массива эффективной.

Вторая проблема — ввод данных в рисованные поля ввода с телефона. Нужно как-то предусмотреть, чтобы клавиатура появлялась в момент, когда пользователь нажал на определённую область холста HTML5.

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

В перспективе было бы интересно объединить OberonJS с фреймворком Electron.