Записка к проекту
"Реализация Рефала на Java-платформе"

1. Общие принципы

1.1. Цель проекта

Главная цель новой реализации Рефала -- исправить основной недостаток существующих: старые системы были в основном предназначены для создания замкнутых программ на Рефале, вызываемых как готовое приложение с командной строки. Средства интеграции с другими языками были развиты слабо. Лишь одна из систем -- Рефал-2 (переживавшая 3 "инкарнации") имела достаточно хорошо документированный интерфейс с Фортраном (на БЭСМ-6), PL/I (на IBM/360) или С (на IBM/PC). Однако, тот "низкоуровневый" стиль интерфейса не удовлетворяет требований сегодняшнего дня: он был рассчитан на то, что программа на языке "низкого уровня" подстраивается под понятия реализации Рефала "ради эффективности" последнего. Теперь тех требований "эффективности" нет. Современный стиль: "все во имя человека", то есть используем мощность компьютеров для создания удобств разработчикам, для удешевления их труда.

В современном мире доминируют объектно-ориентированные языки. Именно с ними и надо интегрироваться. Рефал должен стать инструментом программирования отдельных ("интеллектуальных") подсистем в рамках больших проектов, ведущихся на Яве, C# и т.п.

Главным фактором, определяющим выбор решений, должен стать следующий:

Как можно более "прозрачная", "бесшовная" ("seamless") интеграция с языком Java. Это означает, что описание интерфейса Рефал-Java должно быть как можно более простым, чтобы программистам было легко вызвать функцию на Рефале из программы на Яве и наоборот.

Второе по важности требование проекта:

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

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

См. подробнее в разделе "Состав Рефал-системы".

1.2. Почему Рефал?

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

В 70-е годы Рефал был уникален в своем классе. Ближайшим родственником был Лисп, который по сравнению с Рефалом -- язык низкого уровня (к нему неплохо подходит метафора "рекурсивный язык ассемблера"). Любители низкого уровня выбирали Лисп (а в 80-е годы его потомка -- язык Scheme), а те, кто предпочитал думать, а не кодировать, -- Рефал. Родственные высокоуровневые языки (макро-расширения Лиспа, Snobol и др.) заметно отставали от Рефала по качеству.

В 80-е годы ситуация изменилась. Появилась плеяда функциональных языков высокого уровня: Hope, Miranda, ML (потом он SML, Standard ML), Caml, Haskell. Из этих языков наибольший интерес представляют SML, Caml, Haskel. Их общая черта -- статическая типизация, в то время как Лисп и Рефал были нетипизированными (статически), точнее: типизированными динамически, то есть типы данных хранились в их представлении и проверялись во время счета. Интересно, что новых нетипизированных функциональных языков не появилось.

Общими у этих языков являются такие свойства:

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

Таблица. Классификация функциональных языков по двум ортам: высокий / низкий уровень, типизированный / нетипизированный:

  типизированный нетипизированный
высокий
уровень
SML, Caml,
Haskel и др.
Рефал
низкий
уровень
- Lisp,
Scheme

Типизированные языки предпочтительнее для больших проектов, когда отладка становится особенно затруднительной и ошибки, вылавливаемые типизацией, заметно увеличивают скорость разработки и удешевляют ее. А в небольших или в хорошо структурированных проектах, когда каждую подсистему разумного размера можно разрабатывать и мыслить отдельно, более популярны нетипизированные или слабо-типизированные языки т.н. scripting languages: Visual Basic, JavaScript, Tcl (последний особенно интересен для сравнения с Рефалом) и др. Lisp и Scheme также выполняют роль scripting languages (достаточно вспомнить знаменитый редактор Emacs, расширяемый на Лиспе). Рефал также хорошо подходит на роль scripting language.

И наконец, важнейшим бизнес-фактором является выбор между "чужым" и "своим": между хорошо-отработанным "фирменным" языком и реализацией языка почти одновременно с его внедрением в тесном контакте разработчиков приложения и языка. Это самый рисковый аспект. Здесь можно учитывать такие принципы (в равной мере пригодные для разработки как software, так и hardware):

  1. "чужое" должно быть отличным, понятным и работающим без контакта с авторами; "свое" может быть "сырым", так как авторы, которые берут на себя проблемы, рядом
  2. если нет подходящего "чужого", хорошо проверенного в приложениях аналогичного класса, выгоднее делать "свое", чем мучаться с "чужим", которое невозможно подстроить под себя.

Интересно, что факторы "чужой / свой" коррелируют с "типизированный / нетипизированный". Реализации (особенно компиляторы) нетипизированных языков заметно проще, чем типизированных, а с учетом того, что можно воспользоваться такими развитыми платформами как Java или MS.Net, воплотить Рефал будет намного дешевле чем скажем, SML (особенно без учета полной интеграции в IDEs и богатых средств отладки, которые можно дорабатывать во вторую очередь).

1.3. Задел

Информация по Рефалу имеется на сайте:

(это зеркала одного и того же сайта) и списках рассылки refal@botik.ru и refal-plus@botik.ru, архивируемых здесь:

На настоящий момент имеются такие реализации Рефала на Intel-платформах:

За основу новой реализации Refal-on-Java будет взята система Рефал-6. Ее входной язык близок к Рефалу 5 и Рефалу Плюс, но с некоторыми отличиями. Ее компилятор написан на Рефале-6. Синтаксис языка на первом этапе предлагается менять минимально. На следующих этапах возможно развитие языка. Сразу понадобится добавить понятия модульности Явы (пакеты; именование функций составными именами через точку). Подробности см. ниже.

1.4. Место Рефала-на-Java в объектно-ориентированной среде

Языки, интегрированные в общую объектно-ориентированную среду, характеризуются следующими свойствами:

К новой реализации Рефала не предъявляется требований, чтобы он удовлетворял этим свойствам в полной мере. Взаимодействие программ на Рефале и на Java будет проходить через средства, которые лежат в "пересечении" их понятий:

Подробнее см. ниже в разделе "Технические решения".

В будущем возможно развитие Рефала в сторону все более полного языка-потребителя вплоть до вызова виртуальных методов в объектах.

1.5. Состав Рефал-системы

Полномасштабная Рефал-система должна состоять из следующих подсистем:

(+)
Компилятор с Рефала в Яву
(+)
Runtime
(+)
Подпрограммы, используемые при реализации семантики языка
(+-)
Библиотека функций для Рефала
(+-)
"Пакетные" средства отладки
(–)
Погружение в IDE
(+)
Просто: Вызов компилятора и счета из редакторов
(–)
Сложно: Редактирование с подсказками (вариантов функций на Рефале и других языках)
(–)
Средне: Диалоговая система отладки
(-+)
Документация
(+-)
По инсталляции и использованию системы
  • README
  • Getting Started
(+-)
По входному языку
(+-)
По библиотеке функций для Рефала
(+-)
По интерфейсу с Явой
(–)
Tutorials

Знаками (+), (+-), (-+), (–) помечены 4 уровня приоритетов. На 1-ой очереди делаются только (+), (+-), причем (+-) в "сыром" виде.

На первом этапе ключевым словом является "минимальный". Будут реализованы:

 

2. Технические решения

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

2.1 Входной язык

За основу берется синтаксис и семантика Рефала-6. Вносятся некоторые недостающие элементы Рефала Плюс. Делаются также некоторые расширения, продиктованные взаимодействием с языком Java.

2.2 Модули

Программа на Рефале состоит из модулей, каждый модуль содержит набор именованных сущностей, например, функций (а также, возможно, ящиков, векторов, таблиц, каналов и т.п.)

Модуль в Рефале описывается парой файлов: rfi-файл интерфейса и rfj-файл определений (как в Рефале Плюс). Файл интерфейса содержит объявления сущностей, доступных из других модулей.

Если интерфейсный файл отсутствует, то считается, что он содержит одно объявление:

    $Func main e:String=;

Из других модулей сущности доступны по полному имени вида <имя-модуля>.<простое-имя-сущности>. Возможен также доступ по короткому имени <простое-имя-сущности>, если в теле использующего файла определений имеется предложение

$use <имя-модуля>...;  -- как в Рефале Плюс

Имена модулей в свою очередь тоже могут быть составными, то есть иметь префикс, отделенный точкой. Префикс тоже может быть составным.

Предложение вида (может находится только в начале файла определений)

$import <префикс>.<простое-имя-модуля>;

делает возможным <простое-имя-модуля> в дальнейшем записывать без префикса. Аналогично, предложение

$import <префикс>.*;

дает возможность записывать простым образом все имена модулей, имеющие данный префикс.

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

$package <prefix>;

Префикс определяет путь в структуре каталогов от некоторого корня к каталогу, в котором лежат файлы интерфейса и определений данного модуля.

1-я очередь: Нет интерфейсных файлов и нет предложения $use, но для импорта есть предложение вида

$from <имя-модуля> $import <простое-имя-сущности>,...;

которое дает возможность использовать в теле модуля упомянутые имена сущностей (функций, статических символов-ссылок и т.п.) без указания имени модуля. Для экспорта используется предложение:

$export <простое-имя-сущности>...;

Реализация

Принципы компиляции:

  1. Компилятор получает задание на компиляцию каждого модуля (автоматической подкомпиляции используемых модулей не производится). В командной строке компиляции может быть указано несколько имен модулей, в том числе посредством wildcards.
  2. Модуль отображается в одноименный Java-класс. Префикс модуля - в имя Java-пакета.
  3. Компилятор переводит модуль в Java-класс, имея на входе оба файла самого этого модуля и интерфейсные файлы других модулей, которые в нем используются. Никакие иные данные компилятором не используются. (В 1-й очереди rfj-файл компилируется совершенно независимо).
  4. Предложения $import и $prefix переводятся один к одному (без знака $) в начало файла на java. Кроме того, добавляется предложение import refal;

Таким образом, пользователь может сам написать модуль на Java, снабдив его файлом интерфейса, и его статические методы и объекты будут доступны из Рефала. (В 1-й очереди интерфейсные сущности могут включать только статические методы сигнатурой (Object[]) -> Object[] и статические поля с объектом в качестве значения).

Каждая объявленная сущность модуля, кроме функций, отображается в одноименное статическое поле класса. Функции отображаются в одноименные статические методы.

Чтобы экспортировать функцию как объект, ее нужно дополнительно описать предложением

    $Func <Name>;

тогда в других модулях доступна ссылка на эту функцию как символ:

        *Name    -- как в Рефале-6
или
        &Name    -- как в Рефале Плюс

(В 1-й очереди этот вид экспорта будет порождаться по умолчанию для всех функций, упомянутых в предложении $export).

Аналогичные ссылки на локальные функции модуля можно вводить без дополнительных объявлений.

Экспортируемые имена объявляются public, остальные - private.

2.3 Функции

Функция может иметь предобъявление вида:

    $Func <name> <входной формат> = <выходной формат>;

или, в случае откатной функции:

    $Func? <name> <входной формат> = <выходной формат>;

Если таковое отсутствует, то считается, что функция имеет объявление

    $Func? <name> e=e;

(Внимание: здесь имеется отличие от Рефала Плюс, где по умолчанию функции безоткатные. Это связано с тем, что основу нашего языка составляет Рефал-6, где все функции откатные. Так удобнее при той семантике знака '=', которая имеет место в Рефале-6).

В 1-й очереди возможности описывать формат не будет, все функции будут считаться откатными с форматом e=e. Входной функцией считается функция 

$Func? Main e=e;

Реализация

На основе формата вычисляется сигнатура метода на Java. Каждой переменной входного формата соответствует один параметр. Тип параметра определяется типом переменной и возможным дополнительным спецификатором, например:

Переменная формата Тип параметра
e Object[]

t

Object
s Object
e:SomeClass SomeClass[]
t:SomeClass SomeClass
s:int int
e:float float[]

Для выходного формата тип результата определяется так:

  1. Если в формате нет ни одной переменной, то void (для откатной функции - boolean)
  2. Если в формате ровно одна переменная, то тип результата определяется по таблице. Для откатных функций неудача кодируется как null, если таковое значение для соответствующего типа допустимо, иначе - откаты не допускаются.
  3. Если в выходном формате несколько переменных, то все они объединяются в один массив типа Object[]. Длина массива равна количеству переменных. Примитивные типы заменяются оберточными типами (Integer, Double,...)

Если в интерфейсном файле для функции <name> имеется дополнительное объявление

$Func <name>;

или имя внутренней функции используется в качестве символа-ссылки, то дополнительно к методу порождается одноименное поле

public/private refal.Function <name> =
                new refal.Function("<full-name>") {
                        Object[] eval(Object[] e) {
                            
                                return <name>(e);          

                        }
}

Если функция имеет объявление формата отличное от

$Func? <name> e=e;

то в тело виртуального метода eval вставляются дополнительные операторы преобразования формата, например:

$Func foo s:int: e = s:int;

public refal.Function <name> =
           new refal.Function("module.foo") {
                   Object[] eval(Object[] e) {

                        Integer s1 = (Integer) e[0];
                        Object[] e2 = new Object[e.length-1];
                        System.arraycopy(e,1,e2,0,e.length-1);
                        return new Object[] {new Integer(foo(s1,e2))};          

                        }
}

1-я очередь: если в модуле определена функция Main, то автоматически добавляется метод:

public static void main(String[] args) { Main(args); }

Таким образом функция Main принимает выражение, термами которого являются последовательные параметры командной строки, каждый параметр - слово.

2.4 Данные

Выражение есть последовательность термов. Терм есть либо выражение в скобках, либо символ. Символ есть: литера, число, слово, символ-ссылка. Еще теперь появляется символ null.

Статические символы-ссылки описываются предложением вида

$<тип-ссылки> <символ>,... ;

Например:

$BOX A,B,C;

Реализация

Выражение отображается на массив типа java.lang.Object[]. Произвольный терм - на java.lang.Object. Выражение в скобках - опять-таки массив объектов. Неоднозначность снимается правилом: на верхнем уровне скобки снимаются. Надеюсь, не запутаемся.

Литеры представляются объектами типа java.lang.Character. 

Слова - java.lang.String. 

Числа - Integer, Long, Float, Double, BigInteger, BigDecimal. Запись констант можно сделать как в java (с L на конце - Long, иначе Integer, с f - Float, иначе - Double), для перевода в BigInteger, BigDecimal используется явная функция. Встроенные операции ADD, SUB, MUL, DIV, REM применимы равно к любым видам чисел.

В качестве символов-ссылок могут использоваться любые java-объекты (не массивы).

Тип ссылки - это класс на java. Для каждого символа компилятор создает одноименное статическое поле указанного типа и инициализирует его через конструктор без параметров, который в этом классе должен быть.

Встроенные классы BOX, ТАBLE ... находятся в пакете refal. Поэтому их тип может записываться всегда без префикса.

2.5 Пример компиляции (для 1-й очереди)

Возмем такой код на рефале:

Файл Sequence.rfj:

$from STDIO $import PRINTLN

PurgeEqual {
e1 e2, e2: $r e3 e3 e4 = <PurgeEqual e1 e3>;
e1 = e1;
}

Main e1 = <PRINTLN <PurgeEqual e1>>;

А вот как он будет странслирован в java:

Файл Sequence.java:

public class Sequence {

// PurgeEqual {
//   e1 e2, e2: $r v3 v3 e4 = <PurgeEqual e1 e4>;
//   e1 = e1;
// }

static Object[] PurgeEqual (Object[] e0) {
    int len0 = e0.length;
    for (int i1=0; i1<=len0; i1++) {
        int len2 = len0-i1;
        for(int i3 = len2/2; i3>0; i3--) {
            if (Lang.exprsEqual(e0,i1,e0,i1+i3,i3)) {
                int i4 = 2*i3;
                int len4 = len2 - i4;
                int len5 =  i1 + len4;
                Object[] e5 = new Object[len5];
                System.arraycopy(e0,0,e5,0,i1);
                System.arraycopy(e0,i1+i4,e5,i1,len4);
                Object[] e6 = PurgeEqual(e5);
                return e6;
                }
            }
        }
    return e0;
    }

// Main e1 = <PRINTLN <PurgeEqual e1>>;

static private Object[] Main(Object[] e1) {
    Object[] e2 = PurgeEqual(e1);
    return STDIO.PRINTLN(e2);
    }

public static void main(String[] args) { Main(args); }
}