Back ] Up ]

ОСОБЕННОСТИ СИСТЕМЫ REFAL-JAVA

Введение

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

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

Основные постулаты отображения языка Refal на язык Java таковы:

  • Код на Рефале компилируется в код на Java.
  • Терм рефала - любой Java объект.
  • Выражение рефала - массив типа Object[].
  • Выражение в скобках (скобочный терм) - массив типа Object[].
  • Различие между выражением и выражением в скобках осуществляется по месту использования: если внутри другого выражения, то это скобочный терм, если отдельное - то выражение.
  • Символ рефала - любой объект-не-массив. Массивы других типов (не Object) в настоящее время не допускаются ни в качестве символов, ни в качестве скобочных термов.
  • Символ-литера - объект класса Character.
  • Символ-число - объект класса Integer или BigInteger (последнее - только когда число не умещается в тип Integer).
  • Символ-слово - строка (объект класса String).
  • Символ-ссылка - объект класса org.refal.j.Reference.
  • Объект, на который указывает символ-ссылка - любой Java объект, реализующий интерфейс org.refal.j.Referable.
  • Функция рефала - статический метод типа Object[] от одного аргумента типа Object[]. Функции вызывают друг друга точно так же, как это делают методы в Java (кроме случая вызова через символ-ссылку).
  • Откат из функции - null в качестве значения метода.
  • Модуль рефала - Java класс.

Компилятор переводит каждый рефал-модуль в один файл (класс) на Java. Далее он должен быть оттранслирован компилятором языка Java в один или несколько файлов типа .class. Для выполнения оттранслированных рефал-программ необходима также библиотека классов - пакет org.refal.j, поставляемый в виде исходных текстов на java. Сам компилятор написан на рефале же и поставляется в виде набора .class-файлов пакета org.refal.j.compiler

Отображение данных

Основной тип данных рефала - терм. Последовательность любого числа термов составляет выражение. Каждый терм - либо выражение в скобках, либо символ.

Основной тип данных языка Java - объект (Object). Последовательность объектов составляет массив (типа Object[]). Каждый объект (Object) есть либо массив, либо экземпляр некоторого класса.

Сопоставляя две предыдущие фразы, получаем следующее соответствие: 

Refal

Java

Терм

объект, тип Object

Выражение

массив, тип Object[]

Символ

экземпляры классов, тип Object, но не массив

Конкретные классы символов рефала реализуются определенными классами Java:

Класс символа Рефала

Класс Java

Символ литерa: 'a', '3', '*', '\n', ... java.lang.Character
Слово: Abc, "abc", "a+b", ... java.lang.String
Число целое: 0, 17, -325, ... java.lang.Integer или java.math.BigInteger
Число вещественное: 3.14, -0.1,... java.lang.Double
Символ-ссылка: &F, &Out, ... org.refal.j.Reference

Поскольку терм - это Object, мы не можем использовать непосредственно примитивные типы Java, такие как char, int и т.п. Приходится заворачивать их в объекты, благо соответствующие обертки уже существуют в стандартной библиотеке: Character, Integer и т.п.

Поскольку целые числа в рефале традиционно имеют неограниченную разрядность, мы используем java.math.BigInteger. А чтобы не "стрелять из пушек по воробьям", для маленьких чисел используется Integer. При этом система сама определяет после каждой операции, какое представление использовать в зависимости от величины числа. Если число в принципе помещается в Integer, то Integer и используется. Важно, что одинаковые числа всегда представляются одинаковыми классами. (Мы не используем класс Long, чтобы для выполнения сложений и умножений "маленьких" чисел всегда можно было использовать примитивный тип long.) При любом представлении можно считать, что значение относится к классу Number, и пользоваться его методами, например doubleValue().

Проверка символов на равенство (повторные переменные на Рефале) на уровне Java осуществляется методом equals(Object). Для проверки равенства скобочных термов и выражений имеются библиотечные статические методы termsEqual(Object,Object) и exprsEqual(Object[],Object[]) (в классе org.refal.j.Lang).

Символы-ссылки представляются специальным классом org.refal.j.Reference. Символ-ссылка именует место, в котором может находиться некторый объект класса, реализующий интерфейс org.refal.j.Referable. Будем называть его референтным объектом, или просто референтом. В процессе работы этот объект может изменяться, и даже подменяться, но сама ссылка остается неизменной.

В рефале встроенная функция MODE позволяет узнать вид референтного объекта. Понятию вида (MODE) рефала приблизительно соответствует понятие класса Java. Каждый вид представляется объектом определенного класса (обратное, вообще говоря, неверно). Ниже приводится таблица референтных видов, имеющихся в текущей реализации RefalJ, и классов, которыми они представлены.

MODE Ключевое слово объявления в рефале Комментарий Класс Java, реализующий интерфейс org.refal.j.Referable
BOX $BOX Контейнер (термов) org.refal.j.Box
VECTOR $VECTOR Контейнер (термов) org.refal.j.Box
CHAIN $CHAIN Контейнер (термов) org.refal.j.Box
STRING $STRING Контейнер (символов-литер) org.refal.j.Chars
MASK $MASK Контейнер (битов) org.refal.j.Mask
REAL $REAL Вещественная переменная org.refal.j.Real
TABLE $TABLE Таблица (пар: имя->значение) org.refal.j.Table
FILEIO $CHANNEL + инициализация функцией OPEN Канал (CHANNEL) org.refal.j.Stream
FUNC Объявление функции Функция org.refal.j.Function (abstract)

 

Модули

Модуль - отдельно компилируемая единица рефала, занимающая отдельный файл. Разбиение программы на модули обычно преследует следующие цели:

  • выделение смысловых подсистем
  • уменьшение размера одного файла исходного текста
  • независимость сопровождения и развития отдельных подсистем
  • инкапсуляция (скрытие деталей реализации)
  • минимизация связей между частями программы

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

Ключевой вопрос - кем и когда осуществляется привязка использования внешней функции к ее определению: компилятором или загрузчиком. Мы предлагаем два метода связывания, которые отличаются ответом на этот вопрос. По-другому он звучит так: имеет ли компилятор информацию, позволяющую определить для каждого использования внешней функции, в каком модуле находится ее определение. Сразу оговоримся: связыванию подлежат не просто слова: A, False, "a+b", которые отождествляются всегда по своему начертанию независимо от межмодульных границ, а именно функции, то есть их использования (сразу после знака "<") и определения (в начале определений функций). Кроме того использованиями считаются все вхождения в роли символа-ссылки (со знаком "&" впереди).

Связывание при загрузке

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

    $EXTERN name1, name2, ...;

которая сообщает, что имена name1, name2, ... подлежат глобальной идентификации, то есть все вхождения имени функции Foo в модулях, где оно декларировано внешним, принимаются за вхождения одного и того же имени. Если же в каком-то модуле имя Foo внешним не объявлено, то его вхождения в этом модуле считаются отличными от вхождений имени Foo в других модулях.

Объявить функцию внешней в этом же смысле в модуле, где эта функция определяется, можно и другим способом: приписать перед ее определением ключевой слово $ENTRY, например:

    $ENTRY Foo <тело определения функции>;

Это совершенно равносильно тому, чтобы написать:

    $EXTERN ..., Foo, ...;
    ...
    Foo <тело определения функции>;

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

Если при объединении модулей выяснится, что в разных модулях есть два или более определяющих вхождения данного имени XXX, то эта ситуация рассматривается как нештатная, выдается предупреждение (Function XXX redefined), но выполнение продолжается, причем для всех модулей (в том числе для того, где находится альтернативное определение) будет иметь силу только одно определение, а именно - более "позднее".

Именно такая схема реализована в системе Рефал-6. В ней даже имеется возможность создания многоуровневой иерархии модулей, при помощи директив $MODULE - $END в рамках одного файла. Внутри каждого составного модуля действует аналогичная схема связывания, осуществляемая в период компиляции. В системе Refal-J реализована только одноуровневая схема, при которой один файл = один модуль.

При использовании этой схемы вызов внешней функции Foo транслируется в язык Java в виде обращения к системному методу Apply.APPLY, реализующему встроенную функцию рефала APPLY. Ее аргумент в первой позиции содержит символ-ссылку &Foo. Для занесения туда, это значение берется из статической константы, которая определяется и инициализируется в данном модуле (классе) как

[private|public]
static final Reference Foo = Term.getExternal("Foo");

Модификатор private|public выбирается в зависимости от наличия директивы $export Foo. Метод Term.getExternal(name) извлекает из глобального пространства внешних имен символ-ссылку с указанным именем. Если ее еще не было, то создается новый символ, который изначально ссылается в пустоту. В дальнейшем в это место будет положено определение функции, что делается (обычно в статическом блоке) вызовом 

    Foo.defineReferable(new_value);

Здесь аргумент должен иметь тип (интерфейс) org.refal.j.Referable. Если он отличен от null, то он вносится в качестве нового референтного значения символа-ссылки. Если старым референтом ссылки Foo был не null, то печатается предупреждение о переопределении имени. В качестве new_value здесь может выступать объект-функция (см. пример в подразделе "Взаимодействие Refal-Java"), контейнер и т.п. Заметим вскользь, что для символов-ссылок объявленных директивой $BOX или $EXTBOX вызов defineReferable отсутствует, что обусловлено тем, что всякая контейнерная операция предварительно создаст пустой BOX, если данный ей символ-ссылка имеет референт null. Это принципиально для директив $EXTBOX, которые обычно присутствуют с одним и тем же именем сразу в нескольких модулях (в противном случае возникало бы бессмысленное предупреждение о переопределении).

Глобальное пространство имен реализуется через глобальную таблицу &SYSTABLE. Таблица (TABLE) определяет соответствие между именами, в роли которых используются слова (например XXX), и значениями, в качестве которых выступают символы-ссылки (соответственно, &XXX). О работе с таблицами см. "Библиотечные функции. Операции с таблицами".

Связывание при компиляции

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

В этой схеме мы будем пользоваться терминами экспорт/импорт и соответствующими ключевыми словами. Описание импорта имеет вид:

$from MMM $import XXX, YYY, ZZZ; 

Здесь сказано, что определения имен XXX, YYY и ZZZ следует искать в модуле MMM.

Соответственно, в модуле MMM должна содержаться директива

$export XXX, YYY, ZZZ;

При использовании этих средств связывания вызов функции <XXX eA> будет оттранслирован в прямой вызов (статического) метода 

    MMM.XXX(a);

В головном модуле должна содержаться экспортируемая функция Main. Она вызывается из интерпретатора командной строки командой

        rfjava имя_пакета.имя_модуля параметры

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

Модули рефала, как и классы Java, в которые они отображаются, могут входить в пакеты. Для указания имени пакета служит директива:

$package имя_пакета;

Данная директива просто переносится в начало результирующего файла без начального знака "$".

Имя модуля MMM в директиве $from - $import записывается по правилам записи имен классов в языке Java. Это значит, что оно должно содержать полное квалифицированное имя с указанием имени пакета впереди, например:

        org.refal.j.compiler.Rccompj

Имя пакета может опускаться в следующих случаях:

  • модуль (класс) принадлежит тому же пакету, что и текущий модуль
  • модуль (класс) принадлежит пакету java.lang
  • модуль (класс) принадлежит пакету org.refal.j
  • модуль принадлежит пакету, заданному директивой $import

Директива $import имеет формат, аналогичный формату соответствующей директивы Java, перед которой записан знак "$":

    $import <package_name>.*;

или

    $import <pakage_name>.<class_name>;

Первый вариант позволяет опускать имя пакета для всех классов данного пакета, второй - только для указанного. Эти директивы просто переносятся в начало без начального знака "$" (после директивы package, если она есть).

Сочетание схем связывания

В одной программе (наборе совместно работающих модулей) в принципе можно сочетать использование обоих методов связывания. Единственное ограничение, которое необходимо соблюдать - это не объявлять внешним ($extern) имя, которое импортировано директивой $from-$import. Нарушения диагностируются парсером. Но можно объявить имя внешним и одновременно его эскпортировать. Тогда в одних модулях его можно импортировать, а в других использовать через директиву $extern.

Интерактивный режим

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

Команда вызова Run имеет вид:

java org.refal.j.Run M1+M2+...+MK+*Fun A1 A2 ...

где M1, ..., MK - список Ваших модулей, Fun - стартовая функция. Модули загружаются последовательно. Для загрузки используется системный загрузчик (classLoader), вызываемый методом Class.forName(). Конструкция *Fun может находится в любом месте списка. В этом случае она исполняется в соответствующий момент в порядке выполнения серии загрузок.

Параметры A1 A2 ... передаются запускаемой программе. Программа получает их с помощью обращений к встроенной функции <ARG sn>, где sn - номер аргумента (число 1, 2, ...). Таким способом в рефале-6 принято запускать стартовую функцию GO. Вызов <ARG 0> возвращает строку, представляющую текст:

                                M1+M2+...+MK+*Fun

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

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

Пример вызова класса Run:

java -classpath .;%REFALJ_HOME%/lib org.refal.j.Run *ask

При этом будут доступны все встроенный функции и только они, например:

#>add 1 2
3

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

Отладка

Для применения отладочных средств модуль следует оттранслировать с включением отладочного режима (чтобы узнать как, вызовите компилятор без параметров).

Теперь Вы можете включать и выключать трассировку интересующих Вас функций. Это делает функция TRACE. Ее аргумент - список слов, которые являются именами функций. Для отключения трассировки имена следует взять в скобки. Пример:

    <TRACE A B (C)>

- включить трассировку функций А и B и отключить трассировку функции C.

Вы можете задать включение трассировки прямо в модуле, при помощи директивы $EXEC:

$EXEC TRACE F1 F2 G3;

А так можно включать/выключать трассировку в интерактивном режиме:

#>trace A B (C)
Tracing point at A is set
Tracing point at B is set
Tracing point at C is removed

Наконец, Вы можете вставлять вызовы функции TRACE прямо в нужные места Вашей программы.

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

Взаимодействие Refal - Java

Пользователь может написать свой модуль на Java и обращаться к нему из Рефала так же, как если бы он был написан на Рефале. Каждой функции XXX в нем соответствует одноименный статический метод:

    public static Object[] XXX(Object[] a) {
         ...
    }

Кроме того, если надо иметь возможность связыватся с именем функции как с внешней, следует поместить объявление одноименного поля:

  // $ENTRY XXX
    public static final Reference XXX = Term.getExternal("XXX",
        new org.refal.j.Function("MMM.XXX") {
          public Object[] eval(Object[] e) throws Exception {
            return XXX(e); 
          }
        });

где MMM - имя данного класса (для печати при отладке).

В данном случае к этим членам задан доступ public, что дает возможность обращаться к функции ХХХ так, как если бы в рефале она была экспортирована предложением

$export XXX;

В определении метода имя a обозначает аргумент функции (параметр метода). Результат функции должен быть выдан как результат метода. Если при выполнении функции возник откат из функции (неуспех), то метод должен вернуть null.

Мы советуем не набирать все это руками, а прибегнуть к компилятору с Рефала в Java, написав на рефале модуль, содержащий нужные Вам функции с простейшими определениями, например:

$ENTRY XXX;

а потом вписать только реализацию тела метода. В данном случае определяется функция XXX, которая принимает пустой аргумент и выдает пустой результат.

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

Файл MMM.ref:

$ENTRY XXX {
    (e1) = <XXX e1>;
    e1 = e1;
    };

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

Файл MMM.java:

import org.refal.j.*;
public class MMM {
  private static final Reference XXX = Term.getExternal("XXX");

  static { XXX.defineReferable(
    new org.refal.j.Function("MMM.XXX") {
      public Object[] eval(Object[] e) throws Exception {
        return XXX(e);
      } /* eval */
    });
  }

  private static Object[] XXX(Object[] a) throws Exception {
    Error: {
      Object[] result;
      int la = a.length;
      Fail: {
        Norm: {
          M: {
            if (!( la==1 )) break M;
            if (!( (a[0] instanceof Object[]) )) break M;
            Object[] p = (Object[])a[0];
            int lp = p.length;
            Object[] r = new Object[lp+1];
            r[0] = XXX;
            System.arraycopy(p, 0, r, 1, lp);
            Object[] r_1 = Apply.APPLY(r);
            if ( r_1==null ) break Error;
            result = r_1;
            break Norm;
          } /* M: */
          result = a;
          break Norm;
        } /* Norm: */
        return result;
      } /* Fail: */
    } /* Error: */
    throw new org.refal.j.Error("Unexpected fail");
  } /* XXX */

}

Пояснения:

  • Импортируемый пакет org.refal.j содержит все необходимое для работы результата компиляции, в частности встроенные функции.
  • Здесь члены XXX объявлены приватными, поскольку в рефале функция XXX не была экспортирована, а только объявлена внешней (через модификатор $ENTRY).
  • Объявление поля XXX вводит "XXX" как внешнее имя и помещает в поле XXX значение символа-ссылки &XXX. Это значение уже могло существовать и раньше, например если ранее был загружен модуль-класс, использующий внешнее имя XXX. В противном случае символ ссылка создается здесь заново и помещается в глобальную таблицу внешних имен (&SYSTABLE).
  • Статический инициализатор определяет динамически референт, на который указывает символ-ссылка &XXX. Аргументом метода defineReferable должен быть объект типа org.refal.j.Referable. Здесь проверяется, что ранее значением референта символа-ссылки был null, в противном случае в консоль печатается предупреждение о переопределении. В любом случае референт символа-ссылки становится новым. В дальнейшем он в принципе еще может быть переопределен (с соответствующим предупреждением).
  • В качестве нового референта здесь создается экземпляр нового (анонимного) подкласса класса org.refal.j.Function, который нацелен на выполнение функции XXX.
  • Блок Error завершается бросанием исключения org.refal.j.Error. Это случается, например, когда функция, вызванная в безоткатном контексте (после знака "=") возвращает откат.
  • Блок Fail обычно завершается оператором return null;. Но здесь из этого блока нет выхода (благодаря тому, что имеется предложение с левой частью e1), поэтому таковой оператор отсутствует.
  • Вызов функции &XXX в правой части осуществляется здесь через посредство встроенной функции APPLY (а не напрямую: XXX(p), - как кажется тут  естественным). Это связано с тем, что функция XXX внешняя и, согласно семантике рефала-6, если она будет переопределена в модуле, загруженном позднее (или программно, заменой значения символа-ссылки &XXX), то должно будет использоваться новое определение. Такое возможно только при вызове через символ-ссылку &XXX. Здесь ради этого даже пришлось откопировать выражение e1 в новый массив (чтобы приписать к нему слева символ &XXX).