Problem model¶
The problem is described by three value types you define: Input, Solution and Move. The framework imposes no base class and no member on them; the hooks below are optional and used only by the tools.
Contract¶
| Type | Requirements | Notes |
|---|---|---|
| Input | none | built once, immutable while services are bound to it |
| Solution | copyable, movable | does not reference the Input |
| Move | copyable | does not reference a Solution; default-constructible if the explorer uses a cursor |
Optional hooks¶
Each kind of hook may be written in three ways, tried in this order:
| Hook | Forms, in order | Used by |
|---|---|---|
| read an Input | static Input::read(std::istream&); read_input(std::type_identity<Input>, std::istream&) by ADL; operator>> on a default-constructed Input |
read_input, load_input, Session, TextUI |
| read a Solution | static Solution::read(const Input&, std::istream&); read_solution(const Input&, std::istream&) by ADL; operator>> on Solution{input} |
read_solution, load_solution, Session, TextUI |
| write a Solution | Solution::write(const Input&, std::ostream&) const; write_solution(const Input&, const Solution&, std::ostream&) by ADL; operator<< |
write_solution, save_solution, Session, TextUI |
| describe a value | describe() const member; describe(const T&) by ADL; operator<<; a std::variant, or a union move, by what it holds |
describe, TextUI |
| read a cost | read_cost(const Input&, std::string_view) by ADL; else cost::from_text |
read_cost, Session, TextUI, RunParameters (see Cost) |
| compare solutions | the SolutionManager's equal(const Solution&, const Solution&); operator== on Solution (solution identity) |
the checks of easylocal::testing, Session::check_move_independence, TextUI, reactive tabu list |
| compare moves | operator== on Move |
tests, Session and TextUI |
The TextUI shows a Solution without a describe hook as its write hook writes it.
Reading and writing¶
<easylocal/app/io.hpp> reads and writes values through the hooks, in any
program:
| Function | |
|---|---|
read_input<Input>(std::istream&), load_input<Input>(path) |
an Input |
read_solution<Solution>(input, std::istream&), load_solution<Solution>(input, path) |
a Solution of input |
write_solution(input, solution, std::ostream&), save_solution(input, solution, path) |
writes a Solution |
describe(value) -> std::string |
the text of a value, for people |
The concepts readable_input<Input>, readable_solution<Input, Solution>,
writable_solution<Input, Solution> and describable<T> tell whether a
hook exists; has_describe<T> whether T has a describe of its own, member
or free, not just operator<<. The functions throw std::runtime_error when a stream fails (or
what a hook throws); the file functions report errors as
std::runtime_error naming the file, for save_solution those of the final
flush and close too. write_solution and describe are function objects, so
the argument-dependent lookup of a problem's own hook never finds them.
Convenience bases¶
Two optional, non-virtual bases give a class what it needs to receive the Input and its parameters, so that every service does it the same way:
| Base | Header | Gives |
|---|---|---|
easylocal::input_base<Input> |
<easylocal/utils/input_base.hpp> |
input_type, a constructor from const Input& (the one from a temporary is deleted) and input() |
easylocal::parameters_base<Parameters> |
<easylocal/config/parameters_base.hpp> |
parameters_type, which makes the class configurable, a constructor from the block and parameters() |
class TourLength : public easylocal::input_base<Tsp>
{
public:
using input_base::input_base; // the constructor, which input() reads back
double evaluate(const Tour& tour) const { /* ... input().distance ... */ }
};
- Both are optional: a class that declares the same members itself does just as well, and a cost component that needs neither the Input nor parameters derives from nothing.
- Inherit the constructor with
using input_base::input_base;(orusing parameters_base::parameters_base;). A class that derives data from the Input writes its own constructor instead and passes the Input on:explicit C(const Input& input) : input_base{input}, table_{build(input)} {}. - Derive from each at most once: a cost component with a co-located delta is one class, with one Input.
solution_manager_baseandneighborhood_explorer_basederive frominput_base, so the Input reaches a SolutionManager, an explorer and a cost component the same way.parameters_basedoes not validate the block: the recipe that holds it validates it before every construction.-
A class that takes both is built from the Input first, then the parameters, the order the recipes pass them in:
class WeightedLength : public easylocal::input_base<Tsp>, public easylocal::parameters_base<WeightParameters> { public: WeightedLength(const Tsp& input, WeightParameters parameters) : input_base{input}, parameters_base{std::move(parameters)} {} };
Design choices¶
- Plain values. Entities carry no references to each other, so a Solution can be copied, stored and compared freely, and a Move can be applied to any compatible Solution. Behaviour lives in services (SolutionManager, NeighborhoodExplorer), not in the values.
- Immutable, borrowed Input. Services borrow the Input by
const&for their whole lifetime; binding a temporary Input is rejected. Concurrent runs share only the Input. - Who receives the Input. Three kinds of code, three rules:
- values (Input, Solution, Move) hold no Input;
- services (SolutionManager, NeighborhoodExplorers, cost components, delta cost components) receive it once, in their constructor, when a runner or an app is bound, and keep it; their members take only the solution and the move;
- hooks, free functions that are not services (
read_solution,write_solution,read_cost), receive it as a parameter.
A service is built once per Input, so its constructor is where data derived from the instance is computed, such as the conflicts of each exam or a neighbour list: once per bind, owned by the service that uses it, and kept out of the Input, which stays the instance as read. Passing the Input to every call instead would leave such data nowhere but in the Input itself or in a mutable cache.
- Structural vs. feasible. Validity (see SolutionManager) is about the representation; constraint violations are part of the cost.