[PEAK] Use cases for the priority feature

P.J. Eby pje at telecommunity.com
Wed Aug 18 19:33:02 EDT 2010

At 08:40 PM 8/18/2010 +0200, Christoph Zwerschke wrote:
>Am 18.08.2010 01:56 schrieb P.J. Eby:
> > At 12:37 AM 8/18/2010 +0200, Christoph Zwerschke wrote:
>>>Granted, that should work. But it looks a bit overly complicated
>>>to me, because you have to work with functions getters here,
>>>whereas with the priority parameter you could use the actual
>>>functions and spare some of the overhead.
> >
> > Here's Yet Another Way To Do It, btw:
> >
> > ...
> > def call_highest_priority(results):
> > for quality, method in sorted(results, key=operator.itemgetter(0)):
> >     return method()
>That's good, but it gives the method with the minimum quality. Why not:
>def call_highest_priority(results):
>     return max(results, key=itemgetter(0))[1]()

Right - you get the idea though.

>I liked the approach with the "quality < x" condition better. The 
>only disadvantage I see is that it requires all methods to receive 
>that parameter even though the methods themselves don't actually need it.

There's actually an @expand_as workaround to avoid the extra 
argument, but it's somewhat evil.  I'm also still torn about making 
priorities too easy in the general case -- which tends to make that 
disadvantage an *advantage* in my mind, because then you have to 
decide *ahead of time* you are going to be using priorities in a 
particular scheme!

One thought that's occurred to me is that if AmbiguousMethod errors 
included a friendly text explanation along the lines of, "hey, you 
forgot to define what happens if X and Y are both true; maybe you 
need a method with this rule: ...  and spat out what you'd actually 
need to put in your rule to make it more specific, that would 
actually be more immediately helpful than making somebody have to 
figure it out, as is necessary now.

I guess I'd rather people unthinkingly added more-specific rules, 
than unthinkingly added priorities.  ;-)

IOW, if an ambiguous method error actually *said*: you need a rule 
for the "isinstance(ob, Foo) and hasattr(ob, '__json__')" case, 
that'd be a lot more actionable than, "hey these two things are 
unclear...  you should do something about that." ;-)

That's a whole different ball of wax from knowing in the first place 
that your use case is "select amongst applicable prioritized things, 
and I don't care if equal-priority ones are chosen 
unpredictably".  For that, you can just use the argument approach or 
an @expand_as hack, or even define a MethodList subclass that uses 
priorities (instead of serial numbers) as a disambiguator in its sorting.

For that matter, here's Yet Another Priority Hack:

    def prio(pri, pred):
        return pri, pred

    @when(parse_rule, (object, prio, context, cls))
    def parse_prioritized(engine, predicate, context, cls):
        rule = parse_rule(engine, predicate.pred, context, cls
        return Rule(rule.body, rule.predicate, rule.actiontype, predicate.pri)

Then calling when(somefunc, prio(3, "blah")) makes 3 the sorting 
disambiguator...  though of course you'd then have to have *all* your 
methods prioritized for that to work (the default sequence numbers 
would be quite high).  But then, that would do just fine for 
something you knew in advance you wanted to prioritize.

I guess in summary, my feeling at this point is: there are use cases 
for priorities, but IMO they should be use-case specific, and the 
documentation should propose a couple different ways to do it for 
different sorts of use cases.  But I don't think PEAK-Rules should 
actually *provide* a built-in priority mechanism, since there are so 
many ways to implement a custom one in only a few lines -- heck, the 
argument-based one doesn't require anything except adding an argument 
to your function and methods...  which is just costly enough to make 
you consider whether you should be using it or not.  ;-)

I think, though, that we're basically in agreement that TurboJSON is 
*not* a valid use case for priorities in the first place: at worst, 
it's a use case for being able to replace or delete rules.

(By the way, I added gf.clear() to the current RuleDispatch 
emulation, but the PEAK-Rules equivalent is juse 
"rules_for(f).clear()"...  so if someone really wants to start over, they can.)

ISTM that ambiguity in rules comes from three possible sources:

1. The use case itself involves ambiguity (e.g. "conversion quality")

2. The user has overlooked rules that can overlap (e.g. that 
hasattr('__json__') can occur at the same time as something else)

3. The function will be extended by users, who sometimes want to 
replace or override default rules

We can improve on #2 via better error messages and verification 
tools.  #3 can be fixed with a new 'default' method decorator, and/or 
a straightforward way to say you mean to monkeypatch an existing method.

#1 should really be explicitly declared, perhaps by some sort of 
explicit extension to the @combine_using decorator, or a decorator of 
its own.  (Perhaps a @select_using decorator that operates on the 
level of iterating uncalled methods+metadata, rather than over the 
iteration of results?)

Anyway, at this point I'm strongly leaning towards taking priority() 
out of the PEAK-Rules distribution, and replacing it with:

1. A cleaner way to remove or replace existing method(s)

2. An explanation of how to use the "quality>20" approach in the 
documentation, as part of a general explanation of GF design best practices

3. Better error messages from AmbiguousMethods, that explain exactly 
what case is not covered (and therefore, what condition you should 
use on a disambiguating method)

4. (Maybe) a default() method type for rules that should be ignored 
in case of ambiguity (thereby reducing the need for #1 to be a 
super-easy/wonderful API)

5. (Maybe) Other things such as coverage checker/verifiers.

6. (Maybe) a @select_using mechanism, if we can come up with a 
satisfactory API.  Perhaps something where you define a type for the 
metadata, and then *args and **kw from the when() decorators are 
passed into your metadata type constructor, and then your iterators 
receive (metadata, callable) pairs, e.g.:

def my_metadata_constructor(priority=0):
     return priority

@select_using(my_metatdata_constructor, partial(max, key=itemgetter(0)))

@when("condition", 20)   # or @when("condition", priority=20)

This still needs a lot of thought, preferably directed at some 
clearer use cases.

More information about the PEAK mailing list