Skip to content

Rethinking solutions #3

Description

@matthieu-vergne

Since we are on the "rethinking everything" side, may I suggest to push for a proper generic and simple solution interface?

This is what I started to do in my generalization branch for jMetal 5:
https://github.com/jMetal/jMetal/pull/406/commits

Observations

Here are the facts:

  1. Variables are already a List, so we don't need to add get and set methods to interact with each variable, since the List already provides them.
  2. Objectives are stuck with arrays, which are poorly featured (especially in terms of behavior customization), so we should make them proper List too (which I did here, in a way which does not break the legacy code).
  3. Any additional value, like bounds and other things, are required only by some solutions, so they should not be part of the interface to not bother those which don't need it (and don't take more space they need). I tend to think that constraints are the same: some have constraints, others not, so don't impose constraints on all of them.
  4. The attributes map is the best way to make dynamic extensions of the solution: just design a "property" or "metadata" object which represents an additional property of the solution (constraint, bound, etc.). If this property is specific to the solution, it can use the attributes map to store and retrieve it. If it is a constant, it just returns it (if all your solutions have the same constraints, don't make thousands of them store the same value). If an algorithm requires specific properties, it just ask for them (Function<S, T>). If it needs to write to it, it can require it too (BiConsumer<S, T>). I made something a bit more integrated here.

Suggestion

So basically, without loosing any functionality, the solution interface can be reduced to that:

public interface Solution<T> extends Serializable {
  List<T> variables() ;
  List<Double> objectives() ;
  Map<Object, Object> attributes();
}

And here are the removed methods and their new forms:

Legacy New
solution.getVariable(index) solution.variables().get(index)
solution.setVariable(index, value) solution.variables().set(index, value)
solution.getNumberOfVariables() solution.variables().size()
solution.getObjective(index) solution.objectives().get(index)
solution.setObjective(index, value) solution.objectives().set(index, value)
solution.getNumberOfObjectives() solution.objectives().size()
solution.getAttribute(id) solution.attributes().get(id)
solution.setAttribute(id, value) solution.attributes().put(id, value)
solution.hasAttribute(id) solution.attributes().containsKey(id)
solution.getConstraints() constraintsProperty.read(solution)
solution.getConstraint(index) constraintsProperty.read(solution).get(index)
solution.setConstraint(index, value) constraintsProperty.read(solution).set(index, value)
solution.getNumberOfConstraints() constraintsProperty.read(solution).size()

And like constraints, if you need to add boundaries, just create the dedicated property rather than an interface extension which requires its own implementations:

Legacy New
solution.getLowerBound(index) lowerBounds.read(solution).get(index)
solution.setLowerBound(index, value) lowerBounds.read(solution).set(index, value)
solution.getUpperBound(index) upperBounds.read(solution).get(index)
solution.setUpperBound(index, value) upperBounds.read(solution).set(index, value)

In the case of constraints and boundaries, they all apply to specific variables, so you have to deal with a list of values the same size as the variables list. Which means that for all these properties, you need a single class:

public class VariablesSizedProperty<T> {
  void write(Solution<?> s, List<T> values) {
    // do some size checks here, as much as variables
    s.attributes().put(this, values);
  }
  List<T> read(Solution<?> s) {
    return (List<T>) s.attributes().get(this);
  }
}

Actually, since we only use read tog et the list and then read or write it, we can actually replace the write by an automatic generation. Or even combine the two strategies to provide more flexibility.

What about copy?

If you paid attention, you may have seen that I also removed the copy method from the interface.
So aren't we loosing something here?
Actually, it becomes useless, and here is why.

We have no more extensions of the interface, since all extensions are dealt with properties.
Which means that all the algorithms consume and produce Solution instances.
Not even a S extends Solution, but directly Solution itself.
No need to support extensions at all.

Since all implementations are fine, then the algorithm can just pick the implementation it needs.
Just optimize the choice based on the algorithm itself.
All you need then is for this implementation to have a factory method to copy:

static <T> Solution<T> createCopy(Solution<T> original) {
  // Create the instance based on the original
  // Whatever the original class was
}

If you prefer to keep the possibility to customize the copying process, then rather than having the algorithm decide about it, make it an argument of the algorithm:

UnaryOperator<Solution<T>> copier = // copier given in argument
// ...
Solution<T> copy = copier.apply(solution);

If the user wants to use a specific factory method, he just have to give it as a lambda:

UnaryOperator<Solution<T>> copier = AwesomeSolution::createCopy;
algorithm = new MyAlgorithm(..., copier, ...);

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions