Skip to: Site menu | Main content

Drools

Java Rules Engine

Combinations of same type arguments explosion Print

A common question is for the combinatorial explosion you get when you have parameters of the same type in a rule. (Simon has blogged on this... add link here).

I have a solution, which I will probably add to 2.0, but should be in 3.0.

I think that default behaviour should be to not have the "useless" combinations, but it can be made a rule or set attribute.

The following code shows how this is done.

package org.drools.reteoo;


import org.drools.rule.Declaration;
import org.drools.spi.Condition;
import org.drools.spi.ConditionException;
import org.drools.spi.Tuple;

/**
 * 
 * @author <a href="mailto:michael.neale@gmail.com"> Michael Neale</a>
 * Do not allow pointless cross product combinations of the SAME TYPE.
 * Thanks to Felipe Piccolini for the ideas for this, and Bob McWirther. 
 */
public class CrossProductCondition
    implements
    Condition
{
    
    private Declaration[] declarations;
    
    public CrossProductCondition(Declaration[] sameTypeDeclarations) {
        declarations = sameTypeDeclarations;
    }    
    

    public Declaration[] getRequiredTupleMembers()
    {
        return declarations;
    }

    public boolean isAllowed(Tuple tuple) throws ConditionException
    {
        for ( int i = 0; i < declarations.length - 1; i++ )
        {

            Declaration left = declarations[ i ];
            Declaration right = declarations[ i + 1 ];

            if ( !(compareIdentities( tuple,
                                      left,
                                      right )) )
            {
                return false;
            }

        }
        return true;
    }
    
    private boolean compareIdentities(Tuple tuple,
                                      Declaration left,
                                      Declaration right)
    {
        return System.identityHashCode( tuple.get( left ) ) < System.identityHashCode( tuple.get( right ) );
    }    



}

And the test for it:

package org.drools.reteoo;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.drools.RuleBase;
import org.drools.examples.ClassObjectType;
import org.drools.rule.Declaration;

import junit.framework.TestCase;

public class CrossProductConditionTest extends TestCase
{
    
    /** 
     * Test removing cross products. 
     * 
     * The idea is to reduce useless cross product combinations of parameters of the same type.
     * It is ineffect an optimisation to reduce the effect of an explosion of
     * combinations.
     */
    public void testUniqueParameterCombinations() throws Exception {
        RuleBase ruleBase = new RuleBaseImpl( new Rete( ) );
        WorkingMemoryImpl workingMemory = (WorkingMemoryImpl) ruleBase.newWorkingMemory( );
        
        ClassObjectType stringType = new ClassObjectType(String.class);
        ClassObjectType integerType = new ClassObjectType(Integer.class);
        
        //say the rule has three params
        Declaration d1 = new Declaration("1", stringType, 1);
        Declaration d2 = new Declaration("2", stringType, 2);                
        
        
        //And these are our values
        String s1 = "aaa";
        String s2 = "bbb";
        
        
        CrossProductCondition condition = new CrossProductCondition(new Declaration[] {d1, d2});

        //check that it rejects the same param twice
        Map values = new HashMap();
        values.put(d1.getIdentifier(), s1);
        values.put(d2.getIdentifier(), s1);               
        ReteTuple tuple = new MockReteTuple(workingMemory, values);        
        assertFalse(condition.isAllowed(tuple));
        
        //now check that it accepts only one order of different values
        Map valuesA = new HashMap();
        valuesA.put(d1.getIdentifier(), s1);
        valuesA.put(d2.getIdentifier(), s2);
        
        Map valuesB = new HashMap();
        valuesB.put(d1.getIdentifier(), s2);
        valuesB.put(d2.getIdentifier(), s1);
        
        //and finally the test. Not making any assumptions about the order of the params.
        //interestingly, when I run this in Eclipse, it seems to alternate with each test run...
        //or at least be evenly spread. Must be some time based trick for allocation in the JVM.
        if (condition.isAllowed(new MockReteTuple(workingMemory, valuesA))) {           
            assertFalse(condition.isAllowed(new MockReteTuple(workingMemory, valuesB)));
        } else {
            assertTrue(condition.isAllowed(new MockReteTuple(workingMemory, valuesB)));
        }
        
    }
    
    static class MockReteTuple extends ReteTuple {
        
        private Map values = new HashMap();
        
        /**
         * 
         * @param wm
         * @param idToValue map of IDs to values to return.
         */
        MockReteTuple(WorkingMemoryImpl wm, Map idToValue) {
            super(wm);
            values = idToValue;             
        }

        public Object get(Declaration declaration)
        {
            return values.get(declaration.getIdentifier());            
        }
    }    

}

And finally, how to wire it in with the builder, see the following snippet (Builder.java):

/**
     * Add a <code>Rule</code> to the network.
     * 
     * @param rule
     *            The rule to add.
     * 
     * @throws RuleIntegrationException
     *             if an error prevents complete construction of the network for
     *             the <code>Rule</code>.
     */
    protected void addRule(Rule rule) throws RuleIntegrationException
    {
        Map types = new HashMap();
        for ( Iterator it = rule.getParameterDeclarations().iterator(); it.hasNext(); )            
        {            
            Declaration dec = (Declaration) it.next();
            ObjectType type = dec.getObjectType();
            if (types.containsKey(type)) {
                ArrayList list = (ArrayList) types.get(type);
                list.add(dec);
                
            } else {
                ArrayList list = new ArrayList();
                list.add(dec);
                types.put(type, list);
            }
        }
        
        for (Iterator it = types.values().iterator(); it.hasNext(); ) {
            ArrayList list = (ArrayList) it.next();
            
            if (list.size() > 1) {
                
                CrossProductCondition condition = new CrossProductCondition((Declaration[]) list.toArray(new Declaration[] {}));
                rule.addCondition(condition);
            }
        }