On 30 April 2013 19:12, Phil Steitz <phil.ste...@gmail.com> wrote:

> On 4/30/13 10:27 AM, sebb wrote:
> > On 30 April 2013 17:28, Phil Steitz <phil.ste...@gmail.com> wrote:
> >
> >> On 4/29/13 9:40 AM, Luc Maisonobe wrote:
> >>> Hi all,
> >>>
> >>> Since 2.x series, we have been struggling in several areas with respect
> >>> to algorithms API. The latest change was about optimizer, but it is
> only
> >>> one example among others (solvers, integration, ODE and maybe some
> parts
> >>> of statistics may be concerned by the proposal below).
> >>>
> >>> The various things we want to keep and which are not always compatible
> >>> with each others are :
> >>>
> >>>  1) simple use
> >>>  2) immutability
> >>>  3) good OO design
> >>>  4) compatible with reference algorithms implementations
> >>>  5) maintainable
> >>>  6) extensible
> >>>  7) backward compatibility
> >>>  8) probably many other characteristics ...
> >>>
> >>> 3) and 4) often don't work together. 1) 6) and 7) are difficult to
> >>> handle at once.
> >>>
> >>> If we look at optimizers, some progress have been with optimizers with
> >>> respect to extensibility and backward compatibility, but simple use was
> >>> clearly left behind as it is difficult to know which optimizer support
> >>> which feature as neither strong typing nor fixed arguments are used
> >>> anymore. However, keeping the older API would have prevented
> >>> extensibility as the combinatorial explosion of arguments increases as
> >>> features are added (and we still need to add several constraints
> types).
> >>>
> >>> If we look at ODE solvers, we are still using the original API from
> >>> mantissa, but when we add a new feature, we add more and more setters,
> >>> thus going farther and farther from immutability, and imposing some
> >>> unwritten scheduling between calls (for example when we set up
> >>> additional equations, we must also set up the initial additional state,
> >>> and the user must set up a way to retrieve the final additional state).
> >>>
> >>> If we look at solvers, we started with some parameters set up during
> the
> >>> call to solve while other were set up at construction time, but this
> >>> repartition has changed along time.
> >>>
> >>> So I would like to suggest a new approach, which has been largely
> >>> inspired by a recent discussion on the [CSV] component about the
> builder
> >>> API (see <http://markmail.org/thread/o3s2a5hyj6xh4nzj>), by an older
> >>> discussion on [math] about using fluen API for vectors (see
> >>> <http://markmail.org/message/2gmg6wnpm5p2splb>), and by a talk Simone
> >>> gave last year at ApacheCon Europe. The idea is to use fluent API to
> >>> build progressively the algorithm adding features one at a time using
> >>> withXxx methods defined in interfaces.
> >>>
> >>> As an example, consider just a few features used in optimization:
> >>> constraints, iteration limit, evaluations limits, search interval,
> >>> bracketing steps ... Some features are used in several optimizers, some
> >>> are specific to univariate solvers, some can be used in a family of
> >>> solvers ... Trying to fit everything in a single class hierarchy is
> >>> impossible. We tried, but I don't think we succeeded.
> >>>
> >>> If we consider separately each features, we could have interfaces
> >>> defined for each one as follows:
> >>>
> >>>   interface Constrainable<T extends Constrainable<T>>
> >>>             extends Optimizer {
> >>>     /** Returns a new optimizer, handling an additional constraint.
> >>>       * @param c the constraint to add
> >>>       * @return a new optimizer handling the constraint
> >>>       * (note that the instance itself is not changed
> >>>       */
> >>>     T withConstraint(Constraint c);
> >>>   }
> >>>
> >>> Basically they would  be used where OptimizationData is used today.
> >>> An optimizer that supports simple bounds constraints and max iterations
> >>> would be defined as :
> >>>
> >>>   public class TheOptimizer
> >>>          implements Optimizer,
> >>>                     Constrainable<TheOptimizer>,
> >>>                     IterationLimited<TheOptimizer> {
> >>>
> >>>     private final int maxIter;
> >>>     private final List<Constraint> constraints;
> >>>
> >>>     // internal constructor used for fluent API
> >>>     private TheOptimizer(..., int maxIter, List<Constraint> list) {
> >>>       ...
> >>>       this.maxIter     = m;
> >>>       this.constraints = l;
> >>>     }
> >>>
> >>>     public TheOptimizer withConstraint(Constraint c) {
> >>>       List<Constraint> l = new ArrayList<Constraint>(constraints);
> >>>       l.add(c);
> >>>       return new TheOptimizer(..., maxIter, l);
> >>>     }
> >>>
> >>>     public TheOptimizer withMaxIter(int maxIter m) {
> >>>       return new TheOptimizer(..., m, constraints);
> >>>     }
> >>>
> >>>  }
> >>>
> >>> So basically, the withXxx are sort-of setters, but they do preserve
> >>> immutability (we do not return this, we return a new object). It is
> easy
> >>> to add features to existing classes and there is no need to shove
> >>> everythin within a single hierarchy, we have a forest, not a tree. When
> >>> looking at the API, users clearly see what the can use and what they
> >>> cannot use: if an optimizer does not support constraint, there will be
> >>> no way to put a constraint into it. If in a later version constraints
> >>> become available, the existing functions will not be changed, only new
> >>> functions will appear.
> >>>
> >>> Of course, this creates a bunch of intermediate objects, but they are
> >>> often quite small and the setting part is not the most
> >>> computation-intensive one. It becomes also possible to do some
> >>> parametric studies on some features, using code like:
> >>>
> >>>   Algorithm core = new Algorithm().withA(a).withB(b).withC(c);
> >>>   for (double d = 0.0; d < 1.0; d += 0.001) {
> >>>     Algorithm dSpecial = core.withD(d);
> >>>     double result = dSpecial.run();
> >>>     System.out.println(" d = " + d + ", result = " + result);
> >>>   }
> >>>
> >>> This would work for someone considering feature A is a core feature
> that
> >>> should be fixed but feature D is a parameter, but this would equally
> >>> well work for someone considering the opposite case: they will simply
> >>> write the loop the other way, the call to withD being outside of the
> >>> loop and the call to withA being insided the loop.
> >>>
> >>> A side effect is also that it becomes possible to copy safely
> algorithms
> >>> by just resetting a feature, even when we don't really know what
> >>> implementation we have. A typical example I have that creates problems
> >>> to me is duplicating an ODE solver. It cannot be done currently, as
> some
> >>> specific elements are required at construction time that depend on the
> >>> exact type of solver you use (tolerance vectors for adaptive stepsize
> >>> integrators). So if for example I want to do some Monte-Carlo analysis
> >>> in parallel and need to duplicate an integrator,
> >>> I would do it as follows:
> >>>
> >>>   void FirstOrderIntegrator[]
> >>>       duplicate(FirstOrderIntegrator integrator, int n) {
> >>>     FirstOrderIntegrator copies = new FirstOrderIntegrator[n];
> >>>     for (int i = 0; i < n; ++i) {
> >>>       copies[i] =
> >>>         integrator.withMaxEvaluations(integrator.getMaxEvaluations());
> >>>     }
> >>>     return copies;
> >>>   }
> >>>
> >>> This kind of API could be extended to several algorithms, so it may be
> >>> set up in a consistend way accross the library. As I wrote at the
> >>> beginning of this message, I first think about root solvers, optimizers
> >>> and ODE.
> >>>
> >>> What do you think?
> >> I don't have experience implementing this pattern, so don't know
> >> what best practices / pitfalls there may be.  IIRC, what you are
> >> getting is formal immutability and more compact code due to withXxx
> >> inline instead of setXxx at the expense of lots of extra instance
> >> creation.  I guess the latter is not something to worry much about
> >> in today's world.  We just have to be careful with the pattern in
> >> the cases where constructors actually do something.  What is not
> >> crystal clear to me is what exactly you get in flexibility beyond
> >> what you would get just adding setters ad hoc (as the withXxx stuff
> >> effectively does).  It is interesting as well to consider the
> >> reasons that we favor immutability and whether or not this approach
> >> actually improves anything (e.g., concurrency: yes, helps; path /
> >> state complexity: not so much).
> >>
> >>
> > Huh? state complexity is surely much reduced if fields cannot be changed
> > after instance construction.
>
> By - naive, syntactical - definition, yes. But what is important is
> the overall state / path complexity of whatever app is using this
> stuff.  It is not clear to me that spreading that mutability over
> lots of intermediary instances is any better than just mutating a
> single instance.
>
>
In the implementations I have seen, the withXxxx methods are only used
during instance creation.
Effectively they are equivalent to a ctor with lots of parameters, and the
created instance has final fields.

i.e. I assume that
Algorithm dSpecial = core.withD(d);
would create a new instance of Algorithm with this.d = d; where d is final.

Of course if that is not the case here, then the withXxxx methods are no
different from renamed setXxxx methods.

Another approach is to use a builder where the withXxx methods update a
temporary class with changes. These then need to be converted to an
immutable instance, e.g. with a build() method.

I've also seen suggestions of using

Algorithm(withXxx().withYyy()...);

where the with methods again update a temporary class; the advantage is
that no build() method is needed.
The compiler processes the with() chain and passes a single class instance
which is then used to create the final object.

Phil
> >
> >
> >> Phil
> >>
> >>
> >>> Luc
> >>>
> >>> ---------------------------------------------------------------------
> >>> To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org
> >>> For additional commands, e-mail: dev-h...@commons.apache.org
> >>>
> >>>
> >>
> >> ---------------------------------------------------------------------
> >> To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org
> >> For additional commands, e-mail: dev-h...@commons.apache.org
> >>
> >>
>
>
> ---------------------------------------------------------------------
> To unsubscribe, e-mail: dev-unsubscr...@commons.apache.org
> For additional commands, e-mail: dev-h...@commons.apache.org
>
>

Reply via email to