9 Stimmen

Beseitigung von Code-Duplizierungen

Ich versuche, eine kleine funktionale Programmierbibliothek für Java zu erstellen (nur um mein eigenes Bedürfnis zu befriedigen). W Funktionen höherer Ordnung für List s, Set s und Map s bin ich auf dieses Problem gestoßen: Die Funktionen, die eine Sammlung nehmen und eine Sammlung desselben Typs zurückgeben, haben fast dieselbe Implementierung und müssen dennoch für jede der Datenstrukturen neu definiert werden - List s, Set s, und Map s.

Hier ist zum Beispiel die Implementierung von map Funktion für List s, und Set s:

public static <A, B> List<B> map(
  List<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  List<B> ys = new ArrayList<B>();
  for(A a : xs) {
    ys.add(transformer.apply(a));
  }
  return ys;
}

public static <A, B> Set<B> map(
  Set<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  Set<B> ys = new HashSet<B>();
  for(A a : xs) {
    ys.add(transformer.apply(a));
  }
  return ys;
}

A filter Funktion:

public static <A> List<A> filter(
  List<? extends A> xs, 
  Func1<? super A, Boolean> predicate
) {
  List<A> ys = new ArrayList<A>();
  for(A a : xs) {
    if(predicate.apply(a)) {
      ys.add(a);
    }
  }
  return ys;
}

public static <A> Set<A> filter(
  Set<? extends A> xs, 
  Func1<? super A, Boolean> predicate
) {
  Set<A> ys = new HashSet<A>();
  for(A a : xs) {
    if(predicate.apply(a)) {
      ys.add(a);
    }
  }
  return ys;
}

Wie aus diesem Beispiel ersichtlich wird, sind die Körper der Implementierungen für Set y List sind fast identisch.

Es gibt sehr viele Funktionen wie map y filter in meiner Bibliothek, und jede davon ist dreimal für jeden Typ von Sammlungen definiert, an denen ich interessiert bin (d. h. List , Set y Map ). Dies führt zu einer Menge Code-Duplikation und Code-Geruch. Ich wollte wissen, ob es eine Möglichkeit in Java gibt, die mir helfen würde, all die Code-Duplikation zu vermeiden.

Für jede Hilfe sind wir dankbar. Danke!

EDITAR:

Func1 ist eine Schnittstelle, die wie folgt definiert ist:

interface Func1<A, B> {
  public B apply(A a);
}

6voto

Tom Hawtin - tackline Punkte 142461
public static <A, B> List<B> map(
  List<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  List<B> ys = new ArrayList<B>();
  map(xy, transformer, ys);
  return ys;
}

public static <A, B> Set<B> map(
  Set<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  Set<B> ys = new HashSet<B>();
  map(xy, transformer, ys);
  return ys;
}
private static <A, B> map(
  Collection<? extends A> xs, 
  Func1<? super A, ? extends B> transformer,
  Iterable<B> ys
) {
  for(A a : xs) {
    ys.add(transformer.apply(a));
  }
}

Auftrag erledigt.

Beachten Sie, dass es typisch für Java-APIs ist, die veränderbare Sammlung zu übergeben, anstatt eine neue in der Methode zu erstellen. Ich persönlich bin kein Fan von Veränderbarkeit auf der Ebene der Sammlung, aber das ist es, womit wir (in Java) arbeiten müssen.

(Ich bin nicht scharf auf A y B als generische Parameter mit dieser Art von Material).

Oder Sie könnten eine Fabrik benutzen:

public static <A, B> List<B> map(
  List<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  return map(xs, transformer, new CollectionFactory<B, List<B>>() {
      public List<B> create() { return new ArrayList<B>(); }
  });
}

public static <A, B> Set<B> map(
  Set<? extends A> xs, 
  Func1<? super A, ? extends B> transformer
) {
  return map(xs, transformer, new CollectionFactory<B, Set<B>>() {
      public Set<B> create() { return new HashSet<B>(); }
  });
}

private interface CollectionFactory<E, C extends Collection<E>> {
    C create();
}

private static <A, B, C extends Collection<B>> C map(
  Iterable<? extends A> xs, 
  Func1<? super A, ? extends B> transformer,
  CollectionFactory<B, C> factory
) {
  C ys = factory.create();
  for(A a : xs) {
    ys.add(transformer.apply(a));
  }
  return ys;
}

(Wenn man sich mit der sinnlosen Ausführlichkeit anonymer innerer Klassen abfinden kann).

Wenn es nicht für Collection dann müssten Sie einen (hässlichen) Adapter einbauen.

Der Vollständigkeit halber (obwohl nicht getestet, könnte mit ein paar Optimierungen zu tun), eine unangenehme Lösung mit Vererbung:

Set<String> strs = hashSets().map(things, formatter);

...

public static <E> Functions<E, Set<E>> hashSets() {
    return new Functions<E, Set<E>>() {
        protected Set<E> createCollections() {
            return new HashSet<E>();
        }
    };
}

public abstract class Functions<E, C extends Collection<E>> {
    protected abstract C createCollection();

    public <S> C map(
      Set<? extends S> xs, 
      Func1<? super S, ? extends E> transformer
    ) {
      C ys = createCollection();
      for(S a : xs) {
        ys.add(transformer.apply(a));
      }
      return ys;
    }

    public <S> C filter(
      List<? extends S> xs, 
      Func1<? super S, Boolean> predicate // Predicate<? super S> might be nicer!!
    ) {
      C ys = createCollection();
      for(A a : xs) {
        if(predicate.apply(a)) {
          ys.add(a);
        }
      }
      return ys;
    }
}

4voto

missingfaktor Punkte 88801

Ich glaube nicht, dass Sie etwas Besseres tun können als das, was Tom in seine Antwort . Java unterstützt keine Typen höherer Art - eine Funktion, die Ihnen helfen kann, über den Auflistungstyp zu abstrahieren und so zu vermeiden, dass Sie denselben Code für jeden der Auflistungstypen duplizieren.

Scala unterstützt diese Funktion, und sie wird in der Standardbibliothek ausgiebig genutzt. Dieses Papier von Adriaan Moors erörtert, wie Scala diese Art von Code-Duplizierung mit Hilfe von höherwertigen Typen vermeidet.

Zwei Screenshots aus dem oben erwähnten Papier:


alt text


alt text

4voto

Tony Morris Punkte 3027

Java hat keinen Polymorphismus höherer Ordnung (auch bekannt als "higher-kinds"), so dass dies innerhalb des Typsystems nicht möglich ist. Viele Java-Programmierer greifen auf XML und/oder Reflexion zurück (d. h. sie entkommen dem Typsystem), um dieses Manko zu beheben.

Scala kann damit umgehen, und was Sie beschreiben, nennt man einen kovarianten Funktor. Dieser recht grundlegende Datentyp (zusammen mit vielen anderen) wurde in der Scalaz-Bibliothek implementiert und umfasst Implementierungen für java.util.*.

Darüber hinaus gibt es viele weitere kovariante Funktoren, die keine Sammlungen sind, und viele weitere Funktoren, die nicht kovariant sind.

Sie können nach "20 Scala-Übungen für Fortgeschrittene" googeln, wenn Sie dieses spezielle Konzept weiter erforschen möchten.

2voto

munificent Punkte 11450

Ich glaube nicht, dass das Typsystem von Java ausgeklügelt genug ist, um dies zu adressieren, aber das von Scala schon. Mit der Version 2.8 der Sammlungsbibliothek wurde ein System entwickelt, das automatisch eine Sammlung des passenden Typs erstellt, basierend auf der Sammlung, mit der man arbeitet. Wenn Sie also aufrufen filter en un List gibt es eine neue List . Rufen Sie an. filter en un Set und Sie erhalten eine Set zurück. Dabei gibt es nur eine einzige Implementierung von filter .

Weitere Informationen finden Sie unter Traversable und die Dinge, die sie verwenden. Ich glaube CanBuildFrom ist der Ort, an dem ein großer Teil der Magie passiert.

1voto

Johannes Rudolph Punkte 34512

Effektiv ist eine Liste nur eine Monade für einen Typ T und damit die Möglichkeit, mehrere Instanzen des Typs zu speichern. Deshalb gelten hier alle üblichen Gesetze der Monaden, so dass man die tous Operationen mit einer bind y return Mitglied.

Es tut mir leid, dass ich im Moment keine Zeit habe, das weiter zu erklären, aber im .NET-Raum haben wir SelectMany und Enumerable.Repeat(1, element) für die gleichen Zwecke. Es gibt eine Menge Informationen darüber.

Jeder Operator (wie z.B. filter in Ihrem Beispiel) kann mit SelectMay jeweils binden.

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X