Monday, December 19, 2011

Dynamic typing in rules : Traits (part 2)

In a previous post, we discussed the experimental feature called "traits", a way to combine strong and weak typing in Drools. A trait is an interface which can be attached ("donned"), even temporarily, to a fact. It is particularly suitable to model roles or temporary behaviors. Let's take, for example, the following fact model:

declare Person
  @Traitable
  code         : String
  name         : String
  address      : String
  balance      : long
  registerDate : Date
  orders       : Collection
end

declare OrderItem
  @Traitable
  itemId       : String
  price        : long
end

//new syntax!
declare trait HasDiscountApplied
  discount     : long
end


declare trait Customer
  code         : String
  balance      : long
  registerDate : Date
  orders       : Collection
end

declare trait GoldenCustomer extends Customer, HasDiscountApplied
  maxExpense   : long  
end

declare trait SeniorCustomer extends Customer, HasDiscountApplied
  wasAwarded   : boolean
end


A shop registers visitor to prepare special dedicated offers. When a visitor makes a purchase, they become Customers. For some reasons, the shop cares about Golden customers (those who spent more than a certain amount of money over the last year) and Senior customers (those who have been loyal for a certain time).
While some of the items on sale may have a discount, golden and senior customers always receive an (additional), personalized discount. Senior customers may even receive a public mention on the website, if they wish so.

The toy example is not accurate, but will serve to discuss some issues. Here, people and items are the only concrete domain entities: Customer, Golden and Senior are all statuses gained (or lost) by people according to some conditions. Likewise, being applied a discount is another transient property of customers and items.

We have seen previously that :

when
  $p : Person( ... )        // some conditions apply
  // exists Order( ... )
then
  don( $p, Customer.class ) // a Customer proxy is insert
end


The proxy will allow to write rules against the fields defined by the Customer interface, but the getters/setters will be remapped internally to the concrete fields of the wrapped core object (the Person, in this case). Notice that, as a side benefit, using interfaces allows to exploit multiple inheritance.

Now, one might wonder what happens when a core class does NOT provide the implementation for a field defined in an interface. We call hard fields those trait fields which are also core fields and thus readily available, while we define soft those fields which are NOT provided by the core class. Hidden fields, instead, are fields in the core class not exposed by the interface.
In our example, code, balance, registerDate and orders are hard fields for Customer provided by Person. GoldenCustomer adds discount (from HasDiscountApplied!) and maxExpense, which are soft fields. Eventually, name is a hidden field.

So, while hard field management is intuitive, there remains the problem of soft and hidden fields. The solution we have adopted is to use a two-part proxy.
Internally, proxies are formed by a proper proxy and a wrapper. The former implements the interface, while the latter manages the core object fields, implementing a name/value map to supports soft fields. The proxy, then, uses both the core object and the map wrapper to implement the interface, as needed. So, you can write:

when
  $sc : SeniorCustomer( $c : code, // hard getter
                        $award : wasAwarded == true // soft getter
                      )        
then
  $sc.setDiscount( ... ); // soft setter
end


The wrapper itself is exposed through the fields special getter available to all trait proxies. It is used to access soft fields as well as hard ones. The wrapper, in fact, mimics soft access to hard fields too, for uniformity.

  $sc : SeniorCustomer( $name : fields[ "name" ],
                        $code : fields[ "code"],  
                        $award : fields[ "wasAwarded" ] == true 
                      )        


The wrapper, then, provides a looser form of typing when writing rules. However, it has also other uses. The wrapper is specific to the object it wraps, regardless of how many traits have been attached to an object: all the proxies on the same object will share the same wrapper. Secondly, the wrapper also contains a back-reference to all proxies attached to the wrapped object, effectively allowing traits to see each other. To this end, we have provided the new isA operator:

  $sc : SeniorCustomer( wasAwarded == true, 
                        this isA "GoldenCustomer",
                        $maxExpense : fields[ "maxExpense" ]
                      )        


This rule would be triggered by a SeniorCustomer, but propagated only if the fact is "also" ( i.e. the same core object also has donned the trait of ) a GoldenCustomer. The only possible disadvantage is that this type of cross-access requires loose typing, although if an explicit join is always possible thanks to the core field, again common to all traits:

  $sc : SeniorCustomer( wasAwarded == true, 
                        $core : core
                      )        
  $gc : SeniorCustomer( core == $core,
                        $maxExpense : maxExpense
                      )       


Eventually, the business logic may require that a trait is removed from a wrapped object. To this end, we provide two options. The first is a "logical don", which will result in a logical insertion of the proxy resulting from the traiting operation:

then
  don( $x, // core object
       Customer.class, // trait class 
       true // optional flag for logical insertion
     )


The second is the use of the shed keyword, which causes the retraction of the proxy corresponding to the given argument type:

then
  Thing t = shed( $x, GoldenCustomer.class )


This operation returns another proxy implementing the org.drools.factmodel.traits.Thing interface, where the getFields() and getCore() methods are defined. Internally, in fact, all declared traits are generated to extend this interface (in addition to any others specified). This allows to preserve the wrapper with the soft fields which would otherwise be lost.

(Note: in addition to Thing, we also provide the class org.drools.factmodel.trait.Entity, an empty class with just an id and the data structures to make it @Traitable. We might rename it to Individual in the near future. And if this rings two bells to some of you, yes, the reason would be exactly that. Stay tuned.)

4 comments:

  1. So, Sotty, you say about drawbacks of Proxy Facts:

    1. User must refer explicitly the inner customer to access its fields

    To solve this _TYNY_ problem, instead of shallow, simple, obvious and well-known idea of Proxy class (which is fully-supported by Drools debugger and has perfectly predictable behavior), you introduce:
    - five new keywords to the language: don, shed,
    fields, isA, this
    - Trait concept that is not supported nor by Java nor by JVM.
    - some run-time compiler magic - how should I persist my classes from the working memory back to the database? How does your Entity class plays with Hibernate and the rest of J2EE infrastructure?

    Am I the only one who thinks it's a HUGE overkill for a tiny problem?

    2. Status of GoldenCustomer can be applied to instances of Customer only

    Okay, so you explicitly want that GoldenCustomer may refer to ANYTHING, including Products, Sales and Inventory in your working memory? Good, it's your right to shoot yourself in the foot if you really want to discard all the OO conecpts and experience.

    Just do that:

    declare GoldenCustomer
    customer : Object
    // more fields here
    end

    ... and the problem is solved! With no new keywords at all.


    Of course, I understand that I'm not deeply experienced in Drools. So, please, let me know if I missed some important points about your motivation to introduce Traits.

    ReplyDelete
  2. Alexander, from a pure OO+J2EE perspective, you are right, just don't use traits. At least, not yet. By no means this feature is final as is, things might improve and some of its real intended use cases - semantic reasoning ("OWL"), uncertain reasoning (fuzzy, certainty factors, ...) - are still being incubated... I'll blog about them soon

    For the time being, let me try to defend my choices :)
    In a pure-OO version of the GoldenCustomer example, I would use the fact types Customer and GoldenSTATUS. The latter is effectively extending the information about our Customer, with its presence in the WM and the additional data in its fields.

    This is slightly different from wrapping the Customer with a GoldenCustomer. The "problem" is that a plain GoldenCustomer does not wrap the Customer at all.. you still have to mention both patterns explicitly in a rule. Moreover, this works best if
    the Customer and GoldenCustomer fields do not overlap.

    The original idea behind traits, actually, was to provide interface-like constructs rather than classes. Fact would then "don" one or more interfaces at runtime, according to some user-defined (for now) criteria. An object may then don multiple interfaces, possibly part of a multiple-inheritance hierarchy. Interface injection is not a new concept, and there are proposals to extend the JVM in that sense too.

    The rule programmer, then, needs only use the interface when writing the rules: the (real) wrapping is done internally at the implementation level. When a trait interface is "donned", a real transparent proxy is instantiated to wrap the "core" object.

    So, for example, "Parent" can be donned by a Person as well as an Animal. GoldenCustomer may be donned by a Person or by a Company. Whenever you're accessing the name or the SSN of your Parent or GoldenCustomer, you need not worry of what object is actually implementing the role you're matching against. In practice, you'd be writing 2 rules instead of 4...

    I do see your point about persistence: traits are "volatile" in nature, ideally their fields are a subset of the fields of implementing classes, so that there's no need to persist them, but just to re-run the classification rule whenever you de-hibernate a core object. However, it is indeed possible to apply traits to classes providing only a subset of the required attributes. This would be fitting for scenarios where the additional fields are "transient". And even then, consider that the natural persistence layer for those "additional" fields is a triple store, not a relational DB. The possibility of plugging in an external TS, unfortunately, is still on the TODO list: we will probably start with OrientDB.

    This said, one thing that indeed is currently missing is the notion of "disjointness", i.e. a language feature that prevents traits of certain types to be applied to objects already having another type (native or traited). Something like:

    declare GoldenCustomer
    @disjointWith( Wall, ... )
    ...
    end

    This is on the TODO list, too, together with a few other features...

    Davide
    (thanks to conan and mfusco for their suggestions)

    ReplyDelete
  3. How to caputre a Person which is a GoldenCustomer in LHS?

    ReplyDelete
  4. kosiakk I tried that idea and it worked. Good for you for adding on to this. I do like to eliminate steps.

    ReplyDelete