Генерация Java-классов с параметрами значения времени компиляции

10

Рассмотрим ситуацию, когда класс реализует одно и то же базовое поведение, методы и так далее, но для разных целей может существовать несколько разных версий этого класса. В моем конкретном случае у меня есть вектор (геометрический вектор, а не список), и этот вектор может применяться к любому N-мерному евклидову пространству (1-мерное, 2-мерное, ...). Как можно определить этот класс / тип?

Это было бы легко в C ++, где шаблоны классов могут иметь фактические значения в качестве параметров, но у нас нет такой роскоши в Java.

Я могу придумать два подхода для решения этой проблемы:

  1. Наличие реализации каждого возможного случая во время компиляции.

    public interface Vector {
        public double magnitude();
    }
    
    public class Vector1 implements Vector {
        public final double x;
        public Vector1(double x) {
            this.x = x;
        }
        @Override
        public double magnitude() {
            return x;
        }
        public double getX() {
            return x;
        }
    }
    
    public class Vector2 implements Vector {
        public final double x, y;
        public Vector2(double x, double y) {
            this.x = x;
            this.y = y;
        }
        @Override
        public double magnitude() {
            return Math.sqrt(x * x + y * y);
        }
        public double getX() {
            return x;
        }
        public double getY() {
            return y;
        }
    }

    Это решение, очевидно, очень трудоемкое и чрезвычайно утомительное для кода. В этом примере это не так уж плохо, но в моем собственном коде я имею дело с векторами, каждый из которых имеет несколько реализаций, с четырьмя измерениями (x, y, z и w). В настоящее время у меня более 2000 строк кода, хотя каждому вектору нужно только 500.

  2. Задание параметров во время выполнения.

    public class Vector {
        private final double[] components;
        public Vector(double[] components) {
            this.components = components;
        }
        public int dimensions() {
            return components.length;
        }
        public double magnitude() {
            double sum = 0;
            for (double component : components) {
                sum += component * component;
            }
            return Math.sqrt(sum);
        }
        public double getComponent(int index) {
            return components[index];
        }
    }

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

В настоящее время я фактически занимаюсь разработкой в ​​Xtend, поэтому, если будут доступны какие-либо решения Xtend, они также будут приемлемы.

Паркер Хойс
источник
Поскольку вы используете Xtend, вы делаете это в контексте Xtext DSL?
Dan1701,
2
DSL отлично подходят для приложений кода поколения. В двух словах, вы создаете небольшую грамматику языка, экземпляр этого языка (в данном случае описывающий различные векторы) и некоторый код, который выполняется при сохранении экземпляра (генерируя код Java). На сайте Xtext много ресурсов и примеров .
Dan1701
2
Есть идеальное решение этой проблемы с использованием зависимых типов (это более или менее то, для чего они были созданы), но, увы, это не доступно в Java. Я бы пошел с первым решением, если у вас есть только небольшое фиксированное количество классов (скажем, вы используете только 1-, 2- и 3-мерные векторы), и последнее решение для более чем этого. Очевидно, я не могу сказать наверняка без запуска вашего кода, но я не думаю, что это повлияет на производительность, о которой вы беспокоитесь
gardenhead
1
Эти два класса не имеют одинакового интерфейса, они не полиморфны, но вы пытаетесь использовать их полиморфно.
Мартин Спамер
1
Если вы пишете математику по линейной алгебре и беспокоитесь о производительности, то почему именно Java? Я не вижу ничего, кроме проблем в этом.
Sopel

Ответы:

1

В таких случаях я использую генерацию кода.

Я пишу Java-приложение, которое генерирует реальный код. Таким образом, вы можете легко использовать цикл for для генерации множества разных версий. Я использую JavaPoet , что делает его довольно простым для создания реального кода. Затем вы можете интегрировать запуск генерации кода в вашу систему сборки.

Уинстон Эверт
источник
0

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

Вам просто не нужно беспокоиться о производительности с таким примитивом Java-массива. Мы генерируем матрицы с размерами верхней границы 100 столбцов (читай: 100 пространственных векторов) на 10 000 строк, и у нас была хорошая производительность с гораздо более сложными векторными типами, чем в вашем решении 2. Вы можете попробовать заклеить класс или методы маркировки как финальные чтобы ускорить это, но я думаю, что вы оптимизируете преждевременно.

Вы можете получить некоторую экономию кода (за счет производительности), создав базовый класс для совместного использования вашего кода:

public interface Vector(){

    abstract class Abstract {           
        protected abstract double[] asArray();

        int dimensions(){ return asArray().length; }

        double magnitude(){ 
            double sum = 0;
            for (double component : asArray()) {
                sum += component * component;
            }
            return Math.sqrt(sum);
        }     

        //any additional behavior here   
    }
}

public class Scalar extends Vector.Abstract {
    private double x;

    public double getX(){
        return x;
    }

    @Override
    public double[] asArray(){
        return new double[]{x};
    }
}

public class Cartesian extends Vector.Abstract {

    public double x, y;

    public double getX(){ return x; }
    public double getY(){ return y; }

    @Override public double[] asArray(){ return new double[]{x, y}; }
}

Тогда, конечно, если вы используете Java-8 +, вы можете использовать интерфейсы по умолчанию, чтобы сделать это еще труднее:

public interface Vector{

    default public double magnitude(){
        double sum = 0;
        for (double component : asArray()) {
            sum += component * component;
        }
        return Math.sqrt(sum);
    }

    default public int dimensions(){
        return asArray().length;
    }

    default double getComponent(int index){
        return asArray()[index];
    }

    double[] asArray();

    // giving up a little bit of static-safety in exchange for 
    // runtime exceptions, we can implement the getX(), getY() 
    // etc methods here, 
    // and simply have them throw if the dimensionality is too low 
    // (you can of course do this on the abstract-class strategy as well)

    //document or use checked-exceptions to indicate that these methods throw IndexOutOfBounds exceptions (or a wrapped version)

    default public getX(){
        return getComponent(0);
    }
    default public getY(){
        return getComponent(1);
    }
    //...


    }

    //as a general rule, defaulted interfaces should assume statelessness, 
    // so you want to avoid putting mutating operations 
    // as defaulted methods on an interface, since they'll only make your life harder
}

В конечном счете, у вас нет выбора с JVM. Конечно, вы можете написать их на C ++ и использовать что-то вроде JNA для их соединения - это наше решение для некоторых быстрых матричных операций, где мы используем MKL от Fortran и Intel, но это только замедлит процесс, если Вы просто пишете свою матрицу на C ++ и вызываете ее получатели / установщики из Java.

Groostav
источник
Моя главная проблема не в производительности, а во время компиляции. Я действительно хотел бы решение, где размер вектора и операции, которые могут быть выполнены с ним, определяются во время компиляции (как с шаблонами C ++). Возможно, ваше решение лучше, если вы имеете дело с матрицами, которые могут иметь размер до 1000 компонентов, но в этом случае я имею дело только с векторами размером 1 - 10.
Паркер Хойс
Если вы используете что-то вроде первого или второго решения, вы можете создать эти подклассы. Сейчас я тоже просто читаю на Xtend, и это похоже на Kotlin. С Kotlin вы, вероятно, можете использовать data classобъекты, чтобы легко создать 10 векторных подклассов. С Java, если вы можете перенести все свои функции в базовый класс, каждый подкласс займет 1-10 строк. Почему бы не создать базовый класс?
Groostav
Приведенный мною пример упрощен, в моем реальном коде есть много методов, определенных для Vector, таких как произведение векторов, сложение и умножение по компонентам и так далее. Хотя я мог бы реализовать их, используя базовый класс и ваш asArrayметод, эти различные методы не были бы проверены во время компиляции (вы могли бы выполнить скалярное произведение между скалярным и декартовым вектором, и он скомпилировался бы нормально, но потерпел неудачу во время выполнения) ,
Паркер Хойс
0

Рассмотрим перечисление с каждым именованным вектором, имеющим конструктор, который состоит из массива (инициализированного в списке параметров с именами измерений или аналогичными, или, возможно, просто целого числа для размера или пустого массива компонентов - вашего дизайна) и лямбда-выражения для метод getMagnitude. Вы можете сделать так, чтобы перечисление также реализовало интерфейс для setComponents / getComponent (s) и просто установило, какой компонент был каким при его использовании, исключив getX, и др. Вы должны будете инициализировать каждый объект с его фактическими значениями компонентов перед использованием, возможно, проверяя, что размер входного массива соответствует именам или размеру измерений.

Затем, если вы расширите решение до другого измерения, вы просто измените enum и lambda.

Kloder
источник
1
Пожалуйста, предоставьте краткий фрагмент кода вашего решения.
Тулаинс Кордова
0

Исходя из вашего варианта 2, почему бы просто не сделать это? Если вы хотите предотвратить использование необработанной базы, вы можете сделать ее абстрактной:

class Vector2 extends Vector
{
  public Vector2(double x, double y) {
    super(new double[]{x,y});
  }

  public double getX() {
    return getComponent(0);
  }

  public double getY() {
    return getComponent(1);
  }
}
JimmyJames
источник
Это похоже на «метод 2» в моем вопросе. Ваше решение, однако, дает способ гарантировать безопасность типов во время компиляции, однако издержки на создание double[]нежелательны по сравнению с реализацией, которая просто использует 2 примитива double. В таком минимальном примере это похоже на микрооптимизацию, но рассмотрим гораздо более сложный случай, когда задействовано гораздо больше метаданных, а рассматриваемый тип имеет короткое время жизни.
Паркер Хойс
1
Правильно, как говорится, это основано на методе 2. Основываясь на вашей беседе с Groostav относительно его ответа, у меня сложилось впечатление, что ваша забота была не о производительности. Вы количественно оценили эти накладные расходы, т.е. создали 2 объекта вместо 1? Что касается короткого срока службы, современные JVM оптимизированы для этого случая и должны иметь более низкую стоимость GC (в основном 0), чем объекты с более долгим сроком службы. Я не уверен, как метаданные играют в этом. Это метаданные скалярные или размерные?
JimmyJames
Фактический проект, над которым я работал, был структурой геометрии, которая использовалась в гиперпространственном рендерере. Это означает, что я создавал намного более сложные объекты, чем векторы, такие как эллипсоиды, ортотопы и так далее, и преобразования обычно включали матрицы. Сложность работы с многомерной геометрией сделала безопасность типов для размера матрицы и вектора желательной, в то время как все еще существовало желание максимально избегать создания объектов.
Паркер Хойс
То, что я действительно искал, было более автоматизированным решением, которое производило байт-код, похожий на метод 1, что на самом деле невозможно в стандартной Java или Xtend. Когда я закончил, я использовал метод 2, где параметры размера этих объектов должны быть динамическими во время выполнения, и утомительно создавал более эффективные специализированные реализации для случаев, когда эти параметры были статическими. Реализация заменит «динамический» супертип Vectorна более специализированную реализацию (например Vector3), если его время жизни будет относительно большим.
Паркер Хойс
0

Одна идея:

  1. Абстрактный базовый класс Vector, обеспечивающий реализации переменной размерности, основанные на методе getComponent (i).
  2. Индивидуальные подклассы Vector1, Vector2, Vector3, охватывающие типичные случаи, переопределяющие методы Vector.
  3. Подкласс DynVector для общего случая.
  4. Фабричные методы со списками аргументов фиксированной длины для типичных случаев, объявленные как возвращающие Vector1, Vector2 или Vector3.
  5. Заводской метод var-args, объявленный как возвращающий Vector, создающий экземпляры Vector1, Vector2, Vector3 или DynVector, в зависимости от длины arglist.

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

Код скелета:

public abstract class Vector {
    protected abstract int dimension();
    protected abstract double getComponent(int i);
    protected abstract void setComponent(int i, double value);

    public double magnitude() {
        double sum = 0.0;
        for (int i=0; i<dimension(); i++) {
            sum += getComponent(i) * getComponent(i);
        }
        return Math.sqrt(sum);
    }

    public void add(Vector other) {
        for (int i=0; i<dimension(); i++) {
            setComponent(i, getComponent(i) + other.getComponent(i));
        }
    }

    public static Vector1 create(double x) {
        return new Vector1(x);
    }

    public static Vector create(double... values) {
        switch(values.length) {
        case 1:
            return new Vector1(values[0]);
        default:
            return new DynVector(values);
        }

    }
}

class Vector1 extends Vector {
    private double x;

    public Vector1(double x) {
        super();
        this.x = x;
    }

    @Override
    public double magnitude() {
        return Math.abs(x);
    }

    @Override
    protected int dimension() {
        return 1;
    }

    @Override
    protected double getComponent(int i) {
        return x;
    }

    @Override
    protected void setComponent(int i, double value) {
        x = value;
    }

    @Override
    public void add(Vector other) {
        x += ((Vector1) other).x;
    }

    public void add(Vector1 other) {
        x += other.x;
    }
}

class DynVector extends Vector {
    private double[] values;
    public DynVector(double[] values) {
        this.values = values;
    }

    @Override
    protected int dimension() {
        return values.length;
    }

    @Override
    protected double getComponent(int i) {
        return values[i];
    }

    @Override
    protected void setComponent(int i, double value) {
        values[i] = value;
    }

}
Ральф Клеберхофф
источник