Up ]

Методические рекомендации по написанию
на языке Java функций для вызова из Рефала

Представление данных языка Рефал в языке Java

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

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

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

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

Refal

Java

Объектный терм

объект, тип Object

Объектное выражение

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

Объектный символ

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

Каждый объектный символ является объектом какого-то класса. Верно и обратное - любой объект какого-то класса может считаться объектным символом. При этом равными символами считаются такие пары объектов x и y, для которых вызов метода x.equals(y) вырабатывает true. Таким образом, на уровне символов имеет место полный изоморфизм.

На уровне выражений и массивов такого полного соответствия нет. Во-первых, не любые массивы могут рассматриваться как выражения, а только массивы типа Object[]. Точнее, такие массивы m, для которых (m instanceof Object[]) выдает true. В частости, это не null. Но могут быть массивы других конкретных классов или интерфейсов, например, String[]. Длина массива может быть любой, включая нуль. Длина массива в Java - это в точности количество термов на верхнем уровне скобок объектного выражения. 

Во-вторых, значения элементов должны быть правильными термами, то есть символами (любой объект) или выражениями в скобках (массив типа Object[]), причем не null. Если все-таки в Рефал случайно попадет выражение-массив с элементом null, то может произойти нечто непредсказуемое, например NullPointerException (но неизвестно, где и когда).

Наконец, в третьих, Рефал требует, чтобы все выражения, однажды создавшись, никогда не изменялись. К сожалению, это никак нельзя проконтролировать (не хватает в Java понятия immutable array). Сказанное не относится к объектам-символам: если класс допускает изменение поля, то оно вправе изменяться, Рефал здесь дополнительного ограничения не накладывает. Но "правильными" следует считать только такие объекты, у которых все поля объявлены final, а значения полей - либо примитивные либо тоже "правильные". Такие значения не изменяются во времени, а потому называются чистыми, или функциональными значениями. Все собственно рефальские символы (литеры, числа, слова) являются функциональными. Символы-ссылки, вообще говоря, функциональными не являются.

Заметим, что представления выражения в скобках как объектного терма ничем не отличается от содержимого этих скобок как объектного выражения: и то и другое представляется массивом типа Object[], содержащего последовательность термов этого выражения. Это связано с тем, что при обработке на java (в отличие от Рефала, где терм и выражение из одного терма - это одно и то же) мы всегда знаем (хотя бы по типу значения: 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

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

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

С символами-ссылками в Рефале связана особая система операций, поэтому для них введен специальный тип - Reference (из пакета org.refal.j). Эта система унаследована из прежних реализаций Рефала, в частности - Рефала-6, и включена в реализацию Рефал-Java из соображений совместимости. Однако, в системе Refal-Java с объектами удобнее (и эффективнее!) работать непосредственно, считая "ссылкой" на объект само значение соответствующего типа (поскольку в java значениями объектных типов фактически являются ссылками на объекты, а не "самими" объектами). Именно так мы и предлагаем поступать, а потому подробности, связанные с использованием символов-ссылок опускаем, отсылая читателя к разделу документации "Особенности системы REFAL-JAVA".

Вызов методов Java из Рефала

В настоящее время из Рефала непосредственно могут вызываться только статические методы. Более того, они должны иметь строго определенный формат по параметрам и результатам:

    static public Object[] <Name> (Object[] e) [throws Exception];

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

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

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

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

Метод может содержать указание throws Exception. Но, по правилам java, он может и не содержать это указание, или содержать его с более узким классом. Для нас важно, что со стороны Рефала допускаются  любые бросаемые методами исключения.  С другой стороны, если Вы из метода java вызываете функцию, написанную на Рефале, то Ваш код обязан отработать любое исключение (или выпустить наружу).

Вызов из Рефала метода, написанного на java ничем не отличается от вызова обычной функции Рефала. Вы только должны объявить в Вашем модуле данный метод как импортируемый, указав имя класса, в котором он определен. Например, если в классе Access определены методы:

static public Object[] Value (Object[] e) [throws Exception] {...}

static public Object[] Size (Object[] e) [throws Exception] {...}

то в своем рефал-модуле опишите их как

$FROM Access $IMPORT Value, Size;

и вызывайте их как обычные рефал-функции: <Value ...> <Size ...>.

Если класс принадлежит пакету, то имя этого пакета следует указать в предложении import:

$import org.refal.j.*;

смысл и запись (не считая первого символа $) которого полностью аналогичны смыслу и записи соответствующей декларации языка Java. (Хотя именно для этого пакета делать это не обязательно: компилятор Рефала в Java вставляют такую декларацию автоматически).

Все стандартные библиотечные функции Рефала определены именно так в различных классах пакета org.refal.j. Эти функции называются встроенными, поскольку компилятору известен их список и, в частности, известно в каких модулях они находятся. Поэтому пользователю не нужно указывать для них предложения $from - $import.

Рекомендации по написанию методов

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

При выборе формата аргумента старайтесь придерживаться правила, что каждый элемент аргумента - это отдельный параметр. Например, если в Рефале функция X имеет формат обращения  <X s.A t.1 (e.Z)>, то аргументом метода X будет массив из трех элементов. Мы не рекомендуем использовать форматы с e-переменными на верхнем уровне, например <X s.A t.1 e.Z> (кроме случая единственной е-переменной), хотя в принципе это вполне возможно. В этом случает аргументом был бы массив с неопределенным количеством элементов (не менее двух), а переменная e.Z представлялась бы частью массива, а не целым массивом. То же касается формата результата.

Вызов из Java функций, написанных на Рефале

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

Пример:

Файл M.ref:

$Export Rev;
Rev {
    s.1 e.2 = <Rev e2> s1;
    =;
    }

Вызов из java:

    Object[] a = { "A", "B", "C"};
    Object[] res = M.Rev(a);

Результатом будет новый массив {"C","B","A"}, старый массив а при этом не изменяется.

Примеры функций на Java для вызова из Рефала

Пример из библиотеки Рефала: доступ к библиотечным классам языка Java.

В качестве примера машинных операций рассмотрим некоторые функции, определенные в классе Access. Этот модуль содержит функции доступа к объектам-коллекциям, созданным средствами стандартного Java API: это объекты классов, реализующих интерфейсы jva.util.Map или java.util.List. Хотя это различные и не расширяющие друг друга интерфесы в java, мы определяем эти операции так, что они одинаково применимы к обоим интерфейсам. Такой подход уместен для Рефала, так как в нем никак не ограничиваются типы аргументов.

Такие объекты могут быть созданы и напрямую из Рефала при помощи встроенной функции NEWOBJ:

    <NEWOBJ s.className> => s.obj

где s.className - слово, определяющее полное имя класса, например "java.util.ArrayList". Указанный класс должен иметь конструктор без параметров, которым и будет создан новый объект.

В классе Access определены следующие функции: 

Выдать значение по ключу
    <VALUE  t.ListOrMap t.key > => t.value

Связать значение с ключом
    <LINK   t.ListOrMap t.key t.Value > =>

Все значения
    <VALUES t.CollectionOrMap > => e.values

Все ключи
    <KEYS   t.ListOrMap > => e.values

Количество элементов
    <SIZE   t.CollectionOrMap > => s.size

У всех функций первый аргумент - ключевое множество, которое может быть представлено либо списком List либо отображением Map. В первом случае ключами могут быть только целые числа из диапазона [0 .. size-1]. Во втором - любые объектные символы. В остальном операции идентичны для обоих типов.

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

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

    /**
     * Func VALUE t.ListOrMap t.key = t.value
     */
    public static Object[] VALUE(Object[] a) {
        int la = a.length;
        if (! (la==2)) return null;
        Object r = value(a[0], a[1]);
        return (r==null? null : new Object[] {r} );
    }

В первых двух строках мы проверяем, что аргумент a состоит из двух термов. В противном случае возвращается null, что в логике Рефала означает, что функция на таких аргументах не определена, и потому должен следовать откат, если он возможен, либо авария "Unexpected fail", если откат невозможен. Эта логика обеспечивается на уровне вызывающей функции. 

Такие проверки, как правило, имеют вид:

        if (! условие) return null;

где условие выражает то, что должно иметь место.

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

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

    /**
     * Returns the element for the specified index or key.
     * Returns null
     * if either index does not fit the range of List indices
     * or element with this key does not exist in the Map.
     *
     * @param  Object c either List or Map.
     * @param  Object k index or key of element to return.
     * @return the value for this index or key
     */
    public static Object value(Object c, Object k) {
        if (c instanceof List) {
            List list = (List) c;
            Number n = Arithm.getNumber(k);
            if (n==null) return null;
            try {
                return list.get(n.intValue());
            } catch (IndexOutOfBoundsException ex) {
                return null;
            }
        } else
        if (c instanceof Map) {
            Map map = (Map) c;
            return map.get(k);
        }
        else return null;
    }

После проверки на тип приходится приводить к аргумент c к нужному типу. Затем для случая List необходимо убедиться, что ключ - целое число и взять соответствующее значение. Для этого мы используем специальный метод getNumber() из класса Arithm. Он возвращает результат типа java.lang.Number, если аргумент - число и null в противном случае. Из него извлекаем i=n.intValue(), а затем извлекаем элемент list.get(i). Если i окажется за пределами диапазона [0..size-1], то будет брошено Exception, при котором будет возвращен null. Для случая Map код еще проще, поскольку map.get(k) сам возвращает null, когда элемента с нужным ключом нет.

Функции Values и Size допускают в качестве первого терма помимо Map и List также и любой объект типа Collection, поскольку эти функции относятся к множеству значений и не касаются ключей. Однако, для полноценной работы с множествами в этом наборе функций пока мало.

Пример взаимодействия с базой знаний

Дадим образец написания функций из приложения A на примере функции GetObject. Понятие R-объекта вводится в документе "Взаимодействие ONTOS - Refal". Делить на внешний и внутренний метод не будем, поскольку внутренний по сути состоит из одного вызова метода фабрики.

    /**
     * Func GetObject t.Ontos t.Name = e.Objects
     */
    public static Object[] getObject(Object a[]) {
        int la = a.length;
        if (! (la==2)) return null;
	if (! (a[1] instance of String)) return null;
	TAbstractFactory factory = getFactory(a[0]);
	List list = factory.getObject((String)a[1]);
	return list.toArray();
	}

Вспомогательный метод getFactory извлекает фабрику из аргумента t.Ontos, который есть либо фабрика как таковая, либо некоторый R-объект, из которого можно извлечь его фабрику:

    public static TAbstractFactory getFactory(Object ontos) {
	if (ontos instanceof RDBObject) ontos = ((RDBObject)ontos).getFactory();
	if (ontos instanceof TAbstractFactory) return (TAbstractFactory)ontos;
	return null;
    }

Этот и другие методы из приложения A написаны (но не отлажены, поскольку пока отсутствуют необходимые методы фабрики) в файле ontos.refal.NewOntos.java. Имеет смысл включить их (после первичной отладки) в класс ontos.refal.Ontos.

Полезные библиотечные методы

Класс org.refal.j.Arithm:

static Number getNumber(Object t) - проверяет, является ли терм t числом, и выдает его в виде экземпляра класса Number, в противном случае - null.

static Number norm_Big(BigInteger n) - приводит целое число n, заданное как BigInteger, к каноническому виду: Integer, если значение умещается в int, в остальных случаях - BigInteger.

static Number norm_long(long n)- приводит целое число n, заданное как long, к каноническому виду: Integer, если значение умещается в int, в остальных случаях - BigInteger.

Класс org.refal.j.Lang:

static boolean termsEqual(Object t1, Object t2) - проверяет термы t1 и t2 на равенство, как рефальские термы.

static boolean exprsEqual(Object[] e1, int i1, Object[] e2, int i2, int len) - проверяет подвыражения (e1,i1,len) и (e2,i2,len) на равенство, как рефальские выражения. Здесь (e,i,len) - подмассив массива e длины len, начиная с элемента i.

Приложение A. Работа с объектами онтологии (проект)

Все функции этой группы имеют в качестве первого аргумента объект, по которому может быть определена "данная отнология". Это может быть либо фабрика (TAbstractFactory), либо любой R-объект (из коего всегда может быть извлечена фабрика, которая его построила). Будем обозначать его как s.Ontos.

Объекты отнологии в Рефале представляются как R-объекты класса ontos.kbs.knowledge.TSubject (или его подкласса?).

Функции получения объектов из базы знаний:

Получить объект(ы) по имени

<GetObject s.Ontos s.Name> => e.Objects

Получить объект(ы) по имени и типу (здесь тип - имя одного из родительских концептов)

<GetObect s.Ontos s.Name s.Type>

Получить объект(ы) по имени, типу, при наличии в нем данного(ых) атрибута(ов)

<GetObect s.Ontos s.Name s.Type t.AttrName>

Получить объект(ы) по имени, типу, при условии, что данный атрибут(ы) в нем имеет указанное значение(я)

<GetObect s.Ontos s.Name s.Type t.AttrName t.AttrValue>

Аргумент t.AttrName может быть либо s.AttrName либо (e.AttrNames). В последнем случае аргумент t.AttrValue также имеет формат (e.AttrValues).

Функции извлечения информации из объекта онтологии:

<GetBody s.Object> -> (s.Name (s.AttrName1 t.AttrValue1) ... )

Значением t.AttrValue может быть:

  • Число (целое или вещественное)
  • Строка (слово)
  • Дата
  • Справочник?
  • BLOB
  • CLOB
  • Объект онтологии (ссылка на объект? что такое ссылка?)

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

Смысл методов, их аргументов и результатов аналогичен соответствующим функциям Рефала.

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

Эти методы принадлежат фабрике или ее расширению (?):

List getObjects(String name)

List getObjects(String name, String type)

List getObjects(String name, String type, String[] attrs)

List getObjects(String name, String type, String[] attrs, Object[] values)

Эти методы принадлежат классу TSubject (?):

List getAttributes() выдает список всех атрибутов объекта

Object getValue(String attrName) значение данного атрибута объекта