The PEAK Developers' Center   Diff for "PEAK-Rules" UserPreferences
 
HelpContents Search Diffs Info Edit Subscribe XML Print View
Ignore changes in the amount of whitespace

Differences between version dated 2010-08-12 08:47:52 and 2010-08-17 19:58:51

Deletions are marked like this.
Additions are marked like this.

    Adding overdraft fee of $25
    Transferring 45 from overdraft protection
 
Values
------
 
Custom Method Types
-------------------
Sometimes, if you're defining a generic function whose job is to classify
things, it can get to be a pain defining a bunch of functions or lambdas just
to return a few values -- especially if the generic function has a complex
signature! So ``peak.rules`` provides a convenience function, ``value()``
for doing this::
 
    >>> from peak.rules import value
    >>> value(42)
    value(42)
 
If the standard before/after/around/when decorators don't work for your
application, you can create custom ones by defining your own "method types".
    >>> value(42)('whatever')
    42
 
XXX
    >>> classify = abstract(lambda age:None)
    
    >>> when(classify, "age<2")(value("infant"))
    value('infant')
    
    >>> when(classify, "age<13")(value("preteen"))
    value('preteen')
 
``peak.rules.implies()`` and ``peak.rules.overrides()`` are the generic
functions used to define implication relationships and method overriding, and
they are user-extensible. There are two different engines available: one that
only handles type tuples, and one that supports arbitrary predicates. Using
a string as a condition automatically upgrades a function's engine from one
type to the other.
    >>> when(classify, "age<5")(value("preschooler"))
    value('preschooler')
 
XXX
    >>> when(classify, "age<20")(value("teenager"))
    value('teenager')
 
    >>> when(classify, "age>=20")(value("adult"))
    value('adult')
 
Here's an example of a "pricing rules" generic function that accomodates tax
and discounts as well as upcharges. (Don't worry if you don't understand it at
first glance; we'll go over the individual parts in detail later.)::
    >>> when(classify, "age>=55")(value("senior"))
    value('senior')
    
    >>> when(classify, "age==16")(value("sweet sixteen"))
    value('sweet sixteen')
 
    >>> classify(17)
    'teenager'
 
    >>> from peak.rules.core import Method, MethodList
    >>> from peak.rules.core import always_overrides, combine_actions
    >>> classify(42)
    'adult'
 
    >>> class DiscountMethod(Method):
    ... """Subtract a discount"""
 
Method Combination
------------------
 
The ``combine_using()`` decorator marks a function as yielding its method
results (most-specific to least-specific, with later-defined methods taking
precedence), and optionally specifies how the resulting iteration will be
post-processed::
 
    >>> from peak.rules import combine_using
 
Let's take a look at how it works, by trying it with different ways of
postprocessing on an example generic function. We'll start by defining a
function to recreate a generic function with the same set of methods, so
you can see what happens when we pass different arguments to ``combine_using``::
 
    >>> class A: pass
    >>> class B(A): pass
    >>> class C(A, B): pass
    >>> class D(B, A): pass
 
    >>> def demo(*args):
    ... """We'll be setting this function up multiple times, so we do it in
    ... a function. In normal code, you won't need this outer function!
    ... """
    ... @combine_using(*args)
    ... def func(ob):
    ... return "default"
    ...
    ... def override(self, other):
    ... if self.__class__ == other.__class__:
    ... return self.override(other.tail) # drop the other one
    ... return self.tail_with(combine_actions(self.tail, other))
    ... when(func, (object,))(value("object"))
    ... when(func, (int,)) (value("int"))
    ... when(func, (str,)) (value("str"))
    ... when(func, (A,)) (value("A"))
    ... when(func, (B,)) (value("B"))
    ...
    ... def __call__(self, *args, **kw):
    ... price = self.tail(*args, **kw)
    ... return price - self.body(*args, **kw) * price
    ... return func
 
In the simplest case, you can just call ``@combine_using()`` with no arguments,
and get a generic function that yields the results returned by its methods,
in order from most-specific to least-specific::
 
    >>> func = demo()
 
    >>> list(func(A()))
    ['A', 'object', 'default']
 
    >>> list(func(42))
    ['int', 'object', 'default']
 
In the event of ambiguity between methods, methods defined later are called
first::
 
    >>> list(func(C()))
    ['B', 'A', 'object', 'default']
 
    >>> list(func(D()))
    ['B', 'A', 'object', 'default']
 
Passing a function to ``@combine_using()``, however, makes it wrap the result
iterator with that function, e.g.::
 
    >>> func = demo(list)
 
    >>> func(A())
    ['A', 'object', 'default']
 
While including ``abstract`` anywhere in the wrapper sequence makes the
function abstract (i.e., it omits the original function's body from the defined
methods)::
 
    >>> func = demo(abstract, list)
 
    >>> func(A()) # 'default' isn't included any more:
    ['A', 'object']
    
    >>> discount_when = DiscountMethod.make_decorator(
    ... "discount_when", "Add the result of this calculation"
    ... )
You can also include more than one function in the wrapper list, and they
will be called on the result iterator, first function outermost, ignoring
any ``abstract`` in the sequence::
 
    >>> class AddMethod(MethodList):
    ... """Add the calculated values"""
    ... def __call__(self, *args, **kw):
    ... return sum(body(*args, **kw) for sig,prec,body in self.items)
    >>> func = demo(str.title, ' '.join)
 
    >>> add_when = AddMethod.make_decorator(
    ... "add_when", "Add the result of this calculation"
    ... )
    >>> func(B())
    'B A Object Default'
 
    >>> func = demo(str.title, abstract, ' '.join)
    
    >>> func(B())
    'B A Object'
 
    >>> always_overrides(DiscountMethod, AddMethod)
    >>> always_overrides(AddMethod, Method)
Some stdlib functions you might find useful for ``combine_using()`` include:
 
The ``make_decorator()`` method of ``Method`` objects lets you create decorators
similar to ``when()`` et al.
* ``itertools.chain``
* ``sorted``
* ``reversed``
* ``list``
* ``set``
* ``"".join`` (or other string)
* ``any``
* ``all``
* ``sum``
* ``min``
* ``max``
 
(And of course, you can write and use arbitrary functions of your own.)
 
By the way, when using "around" methods with a method combination, the
innermost ``next_method`` will return the *fully processed* combination of
all the "when" methods, with the "before/after" methods running before and
after the result is returned::
 
    >>> from peak.rules import before, after, around
 
    >>> def b(ob): print "before"
    >>> def a(ob): print "after"
    >>> def ar(next_method, ob):
    ... print "entering around"
    ... print next_method(ob)
    ... print "leaving around"
 
    >>> b = before(func, ())(b)
    >>> a = after(func, ())(a)
    >>> ar = around(func, ())(ar)
 
    >>> func(B())
    entering around
    before
    after
    B A Object
    leaving around
 
XXX
 
We can now use these decorators to implement a generic function::
Custom Method Types
-------------------
 
    >>> @abstract()
    ... def getPrice(product,customer=None,options=()):
If the standard before/after/around/when/combine_using decorators don't work
for your application, you can create custom ones by defining your own "method
types" and decorators.
 
Suppose, for example, that you are using a "pricing rules" generic function
that operates by summing its methods' return values to produce a total::
 
    >>> @combine_using(sum)
    ... def getPrice(product, customer=None, options=()):
    ... """Get this product's price"""
    ... return 0 # base price for arbitrary items
 
    >>> class Product:
    ... @add_when(getPrice)
    ... def __addBasePrice(product,customer,options):
    ... @when(getPrice)
    ... def __addBasePrice(self, customer, options):
    ... """Always include the product's base price"""
    ... return product.base_price
    ... return self.base_price
 
    >>> @when(getPrice, "'blue suede' in options")
    ... def blueSuedeUpcharge(product,customer,options):
    ... return 24
 
    >>> getPrice("arbitrary thing")
    0
 
    >>> shoes = Product()
    >>> shoes.base_price = 42

    >>> getPrice(shoes)
    42
 
And then we can create some pricing rules::
    >>> getPrice(shoes, options=['blue suede'])
    66
 
This is useful, sure, but what if you also want to be able to compute discounts
or tax as a percentage of the total, rather than as flat additional amounts?
 
    >>> @add_when(getPrice, "'blue suede' in options")
    ... def blueSuedeUpcharge(product,customer,options):
    ... return 24
We can do this by implementing a custom "method type" and a corresponding
decorator, to let us mark rules as computing a discount instead of a flat
amount.
 
We'll start by defining the template that will be used to generate our
method's implementation.
 
This format for method templates is taken from the DecoratorTools package's
``@template_method`` decorator. ``$args`` is used in places where the original
generic function's calling signature is needed, and all local variables should
be named so as not to conflict with possible argument names. The first
argument of the template method will be the generic function the method is
being used with, and all other arguments are defined by the method type's
creator.
 
In our case, we'll need two arguments: one for the "body" (the discount
method being decorated) and one for the "next method" that will be called to
get the base price::
 
    >>> def discount_template(__func, __body, __next_method):
    ... return """
    ... __price = __next_method($args)
    ... return __price - (__body($args) * __price)
    ... """
 
Okay, that's the easy bit. Now we need to define a bunch of other stuff to
turn it into a method type and a decorator::
 
    >>> from peak.rules.core import Around, MethodList, compile_method, \
    ... combine_actions
 
    >>> class DiscountMethod(Around):
    ... """Subtract a discount"""
    ...
    ... def override(self, other):
    ... if self.__class__ == other.__class__:
    ... return self.override(other.tail) # drop the other one
    ... return self.tail_with(combine_actions(self.tail, other))
    ...
    ... def compiled(self, engine):
    ... body = compile_method(self.body, engine)
    ... next = compile_method(self.tail, engine)
    ... return engine.apply_template(discount_template, body, next)
    
    >>> discount_when = DiscountMethod.make_decorator(
    ... "discount_when", "Discount price by the returned multiplier"
    ... )
 
    >>> DiscountMethod >> MethodList # mark precedence
    <class 'peak.rules.core.MethodList'>
 
The ``make_decorator()`` method of ``Method`` objects lets you create
decorators similar to ``when()``, that we can now use to add a discount::
 
    >>> @discount_when(getPrice,
    ... "customer=='Elvis' and 'blue suede' in options and product is shoes"

    ... def ElvisGetsTenPercentOff(product,customer,options):
    ... return .1
 
    >>> @add_when(getPrice)
    ... def everything_else_is_free(product, customer, options):
    ... return 0
 
And try them out::
 
    >>> getPrice("something")
    0
    >>> getPrice(shoes)
    42
    >>> getPrice(shoes, options=['blue suede'])
    66
    >>> print getPrice(shoes, 'Elvis',options=['blue suede'])
 
    >>> print getPrice(shoes, 'Elvis', options=['blue suede'])
    59.4
 
    >>> getPrice(shoes, 'Elvis') # no suede, no discount!
    42
 
 
XXX
    This is still pretty hard; but without some real-world use cases for
    custom methods, it's hard to tell how to streamline the common cases.
 
 
Porting Code from RuleDispatch
==============================
 

PythonPowered
ShowText of this page
EditText of this page
FindPage by browsing, title search , text search or an index
Or try one of these actions: AttachFile, DeletePage, LikePages, LocalSiteMap, SpellCheck