Solutions
Overview diagram
ISolution
ISolution
is the most general interface for describing potential solutions in an evolutionary algorithm. It consists of a representation associated with some evaluation. The evaluation reflects the quality of the representation in the context of the problem being solved.
This interface may be specialized to fit more specific classes of problems, i.e. vector problems (TODO ref IVectorProblem), graph problems.
representation
Because of having their representation exposed, solutions are potentially mutable. Although mutability generally isn't a good idea, such a choice was made for efficiency reasons: if we were to create a new solution each time we want to modify its representation (through recombination, mutation or any other way), those would be very short-lived, which would result in a big garbage collection overhead.
You are thus strongly advised to reuse existing solution instances. If you need to set one aside for later use, be sure to make a copy. (see #ISolutionFactory)
Of course, implementations may choose to return immutable representations. Such a case, however, should be a well-documented decision
Beware of equals and hashcode
Reminder for developers: Unless the representation is immutable, do not include it in solutions' equals and hashcode methods. Otherwise, you will break contracts for Java Collections classes.
evaluation
In accordance with the Single Responsibility Principle, solutions should not be aware of the problem being solved. Evaluation is being performed by a dedicated strategy - ISolutionEvaluator
. When asked for their evaluation, solutions may delegate to an implicit evaluator or return some stored value.
Change proposal
In current implementations, asking a solution for its evaluation is simply a shortcut to evaluating it with some implicit evaluator. This might change in future releases: it has been proposed to make evaluation a simple read/write property of solutions, or to decouple solutions entirely from evaluations and turn them into simple representation holders.
ISolutionFactory
ISolutionFactory<S extends ISolution>
is a factory and strategy interface for creating and copying solutions. To keep solutions as simple as possible, the responsibility of creating new instances is given to this dedicated strategy.
Factories can create empty solutions, solutions initialized with respect to some IProblem
(TODO: reference) or copy existing ones. They also decide of representations actual implementation.
ISolutionFactory<S extends ISolution>
is a parameterized type - its type parameter S reflects the type of solutions being created by some given factory.
E.g. ISolutionFactory<IVectorSolution<String>>
creates instances of IVectorSolution<String>
History remark
Prior to version 2.6, factory methods for creating and copying solutions were included in the ISolution
interface. However, given an ISolution
instance you could only get another ISolution
reference. Thanks to return type covariance in Java, specialized solutions could narrow that type.
Yet, if you had a strategy parameterized with some subtype S of ISolution
, you would not get a return type of S, but still ISolution
instead.
Now, you need only have a reference to a ISolutionFactory<S>
to get the correct type of solution in a type-safe manner.
IVectorSolution
IVectorSolution<R>
is the currently only available specialization of ISolution
. It describes traditional evolutionary solutions, i.e. ones with a representation consisting of a vector of features. Indeed, this interface refines the returned representation type to be a list o values.
IVectorSolution<R>
is a parameterized type - its type parameter R reflects the type of features being held in the representation.
E.g. the representation of IVectorSolution<String>
is a list of strings.
Primitives representations
One of java generics limitation is that actual type parameters cannot be primitive ones, i.e. IVectorSolution<Integer>
is needed instead of IVectorSolution<int>
if you want to have a list of integer values as a representation. However, boxed types are much less efficient than primitives, and it has been benchmarked that auto boxing/unboxing such representations all around the computation cause some serious memory and time overheads.
As a solution, current factories for real-valued, integer and binary solutions use Fastutil primitives collections as representations. These collections fit into the general Java Collections hierarchy, but allow specialized primitive operations and are very effective as such. If you can afford to keep a high level of abstraction, you can still pass such a representation as a List<Integer>
references (to keep up with the example), but you can also downcast it to IntList
(which is still an interface) if you need to get or set some int values. This way we achieve a reasonable tradeoff between efficiency and API cleanness
Note however that doing such downcasting is coupling your code very highly to an implementation detail and should be avoided as much as possible. If, for efficiency purposes, you really need to do so, try to keep such code in the same module as the factory creating the representation. That way, assumptions about Fastutil representation will be local to the module using them and won't create coupling across the system.