Generowanie klas Java z parametrami wartości czasu kompilacji

10

Rozważmy sytuację, w której klasa implementuje to samo podstawowe zachowanie, metody itd., Ale istnieje wiele różnych wersji tej klasy do różnych zastosowań. W moim szczególnym przypadku mam wektor (wektor geometryczny, a nie listę) i ten wektor może mieć zastosowanie do dowolnej N-wymiarowej przestrzeni euklidesowej (1 wymiar, 2 wymiar, ...). Jak można zdefiniować tę klasę / typ?

Byłoby to łatwe w C ++, gdzie szablony klas mogą mieć rzeczywiste wartości jako parametry, ale nie mamy tego luksusu w Javie.

Dwa podejścia, które mogę wymyślić, które można zastosować w celu rozwiązania tego problemu, to:

  1. Posiadanie implementacji każdego możliwego przypadku w czasie kompilacji.

    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;
        }
    }

    To rozwiązanie jest oczywiście bardzo czasochłonne i niezwykle uciążliwe dla kodu. W tym przykładzie nie wydaje się to takie złe, ale w moim rzeczywistym kodzie mam do czynienia z wektorami, które mają wiele implementacji, z maksymalnie czterema wymiarami (x, y, z i w). Obecnie mam ponad 2000 wierszy kodu, chociaż każdy wektor naprawdę potrzebuje tylko 500.

  2. Określanie parametrów w czasie wykonywania.

    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];
        }
    }

    Niestety to rozwiązanie szkodzi wydajności kodu, powoduje powstanie bałaganu w porównaniu z poprzednim rozwiązaniem i nie jest tak bezpieczne w czasie kompilacji (nie można zagwarantować w czasie kompilacji, że wektor, z którym mamy do czynienia, jest w rzeczywistości dwuwymiarowy, na przykład).

Obecnie rozwijam się w Xtend, więc jeśli jakiekolwiek rozwiązania Xtend będą dostępne, będą one również do przyjęcia.

Parker Hoyes
źródło
Skoro używasz Xtend, czy robisz to w kontekście DSL Xtext?
Dan1701
2
Listy DSL świetnie nadają się do aplikacji genów kodu. W skrócie, tworzysz małą gramatykę językową, instancję tego języka (w tym przypadku opisującą różne wektory) i trochę kodu, który jest wykonywany po zapisaniu instancji (generowanie kodu Java). Na stronie Xtext znajduje się wiele zasobów i przykładów .
Dan1701
2
Istnieje idealne rozwiązanie tego problemu przy użyciu typów zależnych (mniej więcej do tego, do czego zostały stworzone), ale niestety nie jest dostępne w Javie. Wybrałbym pierwsze rozwiązanie, jeśli masz tylko małą, stałą liczbę klas (powiedzmy, że używasz tylko wektorów 1-, 2- i 3-wymiarowych), a drugie rozwiązanie więcej. Oczywiście nie mogę powiedzieć na pewno bez uruchomienia kodu, ale nie sądzę, że będzie to miało wpływ na wydajność, o którą się martwisz
gardenhead
1
Te dwie klasy nie mają tego samego interfejsu, nie są polimorficzne, ale próbujesz ich używać polimorficznie.
Martin Spamer
1
Jeśli piszesz matematykę algebry liniowej i martwisz się wydajnością, to dlaczego java. Nie widzę w tym nic prócz problemów.
Sopel,

Odpowiedzi:

1

W takich przypadkach używam generowania kodu.

Piszę aplikację Java, która generuje rzeczywisty kod. W ten sposób możesz łatwo użyć pętli for, aby wygenerować kilka różnych wersji. Używam JavaPoet , co sprawia, że ​​budowanie właściwego kodu jest dość proste. Następnie możesz zintegrować generowanie kodu z systemem kompilacji.

Winston Ewert
źródło
0

Mam bardzo podobny model w mojej aplikacji, a naszym rozwiązaniem było po prostu utrzymanie mapy o dynamicznym rozmiarze, podobnej do twojego rozwiązania 2.

Po prostu nie będziesz musiał się martwić wydajnością z takim rodzajem prymitywu java. Generujemy macierze o górnych granicach 100 kolumn (czytaj: 100 wektorów wymiarowych) na 10 000 wierszy i osiągnęliśmy dobrą wydajność przy znacznie bardziej złożonych typach wektorów niż Twoje rozwiązanie 2. Możesz spróbować uszczelnić klasę lub metody znakowania jako ostateczne aby przyspieszyć, ale myślę, że optymalizujesz przedwcześnie.

Możesz uzyskać oszczędności kodu (kosztem wydajności), tworząc klasę podstawową do udostępniania kodu:

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}; }
}

Oczywiście, jeśli korzystasz z Java-8 +, możesz użyć domyślnych interfejsów, aby uczynić to jeszcze bardziej ścisłym:

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
}

Poza tym JVM nie ma możliwości. Możesz oczywiście napisać je w C ++ i użyć czegoś takiego jak JNA, aby je zmostkować - to nasze rozwiązanie dla niektórych szybkich operacji matrycowych, w których używamy MKL fortran i Intel - ale spowolni to tylko, jeśli po prostu piszesz swoją matrycę w C ++ i wywołujesz jej moduły pobierające / ustawiające z java.

Groostav
źródło
Moją główną troską nie jest wydajność, ale sprawdzanie czasu kompilacji. Naprawdę chciałbym rozwiązanie, w którym rozmiar wektora i operacje, które można na nim wykonać, są określane w czasie kompilacji (jak w przypadku szablonów C ++). Być może twoje rozwiązanie jest najlepsze, jeśli masz do czynienia z macierzami, które mogą mieć do 1000 elementów, ale w tym przypadku mam do czynienia tylko z wektorami o wielkości 1 - 10.
Parker Hoyes
Jeśli używasz czegoś takiego jak pierwsze lub drugie rozwiązanie, możesz utworzyć te podklasy. Teraz również czytam o Xtend i wygląda to trochę jak Kotlin. Dzięki Kotlin możesz prawdopodobnie użyć data classobiektów, aby łatwo stworzyć 10 podklas wektorowych. W Javie, zakładając, że możesz przenieść całą swoją funkcjonalność do klasy podstawowej, każda podklasa zajmie 1-10 linii. Dlaczego nie stworzyć klasy podstawowej?
Groostav
Podany przykład jest nadmiernie uproszczony, mój rzeczywisty kod ma wiele metod zdefiniowanych dla Vector, takich jak iloczyn wektorowy kropki, dodawanie i mnożenie składników i tak dalej. Chociaż mógłbym je zaimplementować przy użyciu klasy bazowej i twojej asArraymetody, te różne metody nie byłyby sprawdzane w czasie kompilacji (możesz wykonać iloczyn skalarny między skalarem a wektorem kartezjańskim i kompilowałby się dobrze, ale nie działałby w czasie wykonywania) .
Parker Hoyes,
0

Rozważmy wyliczenie z każdym nazwanym Vectorem posiadającym konstruktor, który składa się z tablicy (zainicjowanej na liście parametrów z nazwami wymiarów lub podobnymi, a może po prostu liczbą całkowitą dla rozmiaru lub pustą tablicą komponentów - twoim projektem) i lambda dla metoda getMagnitude. Możesz mieć enum także zaimplementować interfejs dla setComponents / getComponent (s) i po prostu ustalić, który składnik był w użyciu, eliminując getX, i in. Przed użyciem należy zainicjować każdy obiekt z jego rzeczywistymi wartościami składników, prawdopodobnie sprawdzając, czy rozmiar tablicy wejściowej odpowiada nazwom lub rozmiarom wymiarów.

Następnie, jeśli rozszerzysz rozwiązanie na inny wymiar, po prostu zmodyfikujesz wyliczenie i lambda.

Kloder
źródło
1
Podaj krótki fragment kodu swojego rozwiązania.
Tulains Córdova
0

Opierając się na opcji 2, dlaczego po prostu tego nie zrobić? Jeśli chcesz uniemożliwić korzystanie z surowej bazy, możesz uczynić ją abstrakcyjną:

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
źródło
Jest to podobne do „metody 2” w moim pytaniu. Twoje rozwiązanie daje jednak sposób zagwarantowania bezpieczeństwa typu w czasie kompilacji, jednak narzut związany z tworzeniem a double[]jest niepożądany w porównaniu z implementacją, która po prostu używa 2 prymitywów double. W tak minimalnym przykładzie wydaje się to mikrooptymalizacją, ale rozważmy znacznie bardziej złożony przypadek, w którym zaangażowanych jest znacznie więcej metadanych, a dany typ ma krótki okres użytkowania.
Parker Hoyes,
1
Zgadza się, jak mówi, jest to oparte na metodzie 2. Na podstawie dyskusji z Groostavem na temat jego odpowiedzi mam wrażenie, że nie chodziło ci o wydajność. Czy skwantyfikowałeś ten narzut, tj. Tworząc 2 obiekty zamiast 1? Jeśli chodzi o krótkie okresy użytkowania, nowoczesne maszyny JVM są zoptymalizowane pod tym kątem i powinny mieć niższy koszt GC (w zasadzie 0) niż obiekty o dłuższym okresie życia. Nie jestem pewien, jak odgrywają w tym metadane. Czy te metadane są skalarne czy wymiarowe?
JimmyJames
Rzeczywisty projekt, nad którym pracowałem, stanowił szkielet geometrii do użycia w renderowaniu hiperwymiarowym. Oznacza to, że tworzyłem o wiele bardziej złożone obiekty niż wektory, takie jak elipsoidy, orthotopy i tak dalej, a transformacje zwykle dotyczyły macierzy. Złożoność pracy z geometrią o wyższych wymiarach sprawiła, że ​​bezpieczeństwo typu matrycy i rozmiaru wektora było pożądane, podczas gdy nadal istniała znacząca chęć uniknięcia tworzenia obiektów w jak największym stopniu.
Parker Hoyes
Myślę, że tak naprawdę szukałem bardziej zautomatyzowanego rozwiązania, które wygenerowało kod bajtowy podobny do metody 1, co tak naprawdę nie jest możliwe w standardowej Javie ani Xtend. Kiedy skończyłem, korzystałem z metody 2, w której parametry wielkości tych obiektów musiały być dynamiczne w czasie wykonywania, i żmudnie tworzyłem bardziej wydajne, specjalistyczne implementacje dla przypadków, w których parametry te były statyczne. Implementacja zastąpiłaby „dynamiczny” nadtyp typem Vectorbardziej wyspecjalizowanym (np. Vector3), Gdyby jego żywotność miała być stosunkowo długa.
Parker Hoyes,
0

Jeden pomysł:

  1. Wektor abstrakcyjny klasy bazowej zapewniający implementacje o zmiennym wymiarze oparte na metodzie getComponent (i).
  2. Indywidualne podklasy Vector1, Vector2, Vector3, obejmujące typowe przypadki, zastępujące metody Vector.
  3. Podklasa DynVector dla ogólnego przypadku.
  4. Metody fabryczne z listami argumentów o stałej długości dla typowych przypadków, zadeklarowane do zwrócenia Vector1, Vector2 lub Vector3.
  5. Metoda fabryczna var-args, zadeklarowana do zwrócenia Vector, wystąpienia instancji Vector1, Vector2, Vector3 lub DynVector, w zależności od długości arglisty.

Zapewnia to dobrą wydajność w typowych przypadkach i pewne bezpieczeństwo podczas kompilacji (wciąż można je poprawić) bez poświęcania ogólnego przypadku.

Szkielet kodu:

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;
    }

}
Ralf Kleberhoff
źródło