Blocks In Java Compositors

Prev: BlocksInJavaAst Next: BlocksInJavaPuttingItTogether

Compositors and Binders

Here is where we start to attack my second reason for this package - expression composition. I have a desire to use these interfaces as more than just simple blocks or anonymous inners. For example, I would like to be able to use them as objects each of which can represent a composable chunk of logic, I would also like to be able to aggregate them into efficient expressions. For this, I did go to the work of Alexander Stepanov and David R. Musser (see: STL functors). This approach seemed much simpler and more intuitive to me than the InterpreterPattern, but your mileage may vary. The STL constructs allow me to create executable logic without having to create new specializations. These could then be used in a variety of ways. While it is usually easier to simply create an anonymous inner that implements one of the interfaces for in-code use, the composable functors are great for something like a Rule Language whose bits and pieces of logic can be visually arranged into expressions that can be persisted and executed.

Toward this end, I wanted to be able to create generic constructs such as the following:

 UnaryPredicate inRange = 
     new BinaryCompose(
         new And(),
         new BinderSecond(
             new Greater(),
             new Long(00) ),
         new BinderSecond(
             new Less(),
             new Long(11) ) ) );
This object can then be passed around as a unary predicate that checks to see if some variable is within the range [1,11) (i.e. [1,10]). Now that it is in object form, it can be used as an assertions like so:

 public void foo( Long value )
     Dbg.assert( value ) );
Or with something more complex like a detect iterator to find the first element in a collection of Long objects that is in the specified range:

 Long item = (Long)numbers.detect( inRange );
Of course, if this is a one time thing, you can simply do the following:

 Long item = (Long)
     numbers.detect( new UnaryPredicate() {
         public boolean is( Object each ) {
             long value = ((Long)each).longValue();
             return value > 0 && value < 11; } } );
The only problem with this is that it requires a priori knowledge about the logic and does not allow that logic to be dynamically composed such as by a user using a visual logic builder. If you are not familiar with STL, BinaryCompose aggregates a Binary Predicate with two Unary Predicates into a single Unary Predicate. For example, if "bp" is the binary predicate with "up1" and "up2" as unary predicates, BinaryCompose can be used to create "bc" such that: value );
is the same as evaluating: value ), value ) );
The Binder predicates bind the first or second argument of a Binary Predicate so that it acts as a Unary Predicate. So, a Binary Predicate such as Equals can have one of its arguments bound like so:

  UnaryPredicate equalsJohn =
      new BinderFirst( new Equals(), "John" );

Dbg.assert( sName ) );
This is the same as saying:

  BinaryPredicate equal = new Equals();

Dbg.assert( "John", sName ) );
If you are interested in this stuff, I suggest you read the link I placed in this section on programming with generic algorithms. It's interesting stuff and was implemented in Ada before it ever showed up in C++.

Concrete Building Blocks

Before we can create the classes that adapt and compose functors into expressions, we have to define the building blocks they work on. These are the atomic unary and binary building blocks we all use in expressions. We know them very well, relational (>,<), equality (==,!=), conditional (and,or), arithmetic (*,/,+,-,%), and so on. These are extraordinarily easy to implement so I will just show enough here to get you going. In this document, I am only going to cover what I call the logical operators -- i.e. relational, equality, and conditional.

While it may not be a perfect name, I will first create two classes - UnaryLogicalFunctor and BinaryLogicalFunctor. These classes allow the logical operators to be called either as Functions (ala UnaryFunction) returning a Boolean object or as Predicates (ala UnaryPredicate) returning a boolean primitive. For example, a binary Equal FunctorObject can then be used in the following ways:

  1. Use class Equal as a BinaryFunction:
        BinaryFunction equal = new Equal();
        Boolean b = (Boolean)equal.eval( new Long(1), new Long(1) );
  1. Use class Equal as a BinaryPredicate:
        BinaryPredicate equal = new Equal();
        boolean isEqual = new Long(1), new Long(1) );
Maybe the abstract class that Equal implements might be better named BinaryPredicateFunction, but BinaryLogicalFunctor seemed more appropriate at the time.

Because Java does not have generic types or variants that do not require casts, this feature (calling logical operators as functions or predicates) becomes very important in RealWorld scenarios such as when relational and arithmetic FunctorObjects are composed together. I suppose I could have just eliminated the predicates altogether, but they are so elegant and straightforward (and used often enough) that I figured the simpler interface was worth the more complicated implementation (see: the MIT approach versus the New Jersey approach).

The two logical functor classes have only one purpose - mapping between predicate usage and function usage. Note that these abstract classes would not be required by something like arithmetic operators that should always be implemented as functions.

     class      BinaryLogicalFunctor
     implements BinaryPredicate,
     //* Subclass must implement
     public boolean is( Object arg1, Object arg2 );
     //* Call predicate interface and map to object result
     public Object eval( Object arg1, Object arg2 )
             is( arg1, arg2 ) 
                 ? Boolean.TRUE
                 : Boolean.FALSE;

public abstract class UnaryLogicalFunctor implements UnaryPredicate, UnaryFunction { //* Subclass must implement abstract public boolean is( Object arg );

//* Call predicate interface and map to object result public Object eval( Object arg ) { return is( arg ) ? Boolean.TRUE : Boolean.FALSE; } }
That's all there is to 'em. Now, lets dig into some concrete classes that build on these abstract classes. These are the classes one would most often use when constructing constraints.

Conditional: And

 #package com.tripwire.rdifalco.ast;

final public class And extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return ((Boolean)arg1).booleanValue() && ((Boolean)arg2).booleanValue(); } }

Conditional: Or

 #package com.tripwire.rdifalco.ast;

final public class Or extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return ((Boolean)arg1).booleanValue() || ((Boolean)arg2).booleanValue(); } }

Equality: Value

 #package com.tripwire.rdifalco.ast;

final public class Equals extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return arg1.equals( arg2 ); } }

Equality: Reference

 #package com.tripwire.rdifalco.ast;

final public class Same extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return arg1 == arg2; } }

Relational: Less

 #package com.tripwire.rdifalco.ast;

final public class Less extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return ((Comparable)arg1).compareTo( arg2 ) < 0; } }

Relational: Greater

 #package com.tripwire.rdifalco.ast;

final public class Greater extends BinaryLogicalFunctor { public boolean is( Object arg1, Object arg2 ) { return ((Comparable)arg1).compareTo( arg2 ) > 0; } }

Logical: Not

 #package com.tripwire.rdifalco.ast;

final public class Not extends UnaryLogicalFunctor { public boolean is( Object arg ) { return ((Boolean)arg).booleanValue() == false; } }
You get the idea. Please note that I define these building block operators as final classes. You may not like this restriction. My rationale is that one cannot extend the implementation of And or Equals. If one needs a specialized And (whatever that may be), one should simply implement a new BinaryLogicalFunctor. However, this detail is up to you and is not a major issue in the design of the AST package.

Implementing Composers and Binders in Java

We can use this as is. However, if we really want to be able to dynamic construct expressions without writing code, we will need some adaptors that will help us construct these bits into expressions. In this document, I am going to show two specific types of adaptors -- Composers and Binders. The compositors take two functions and chains them together, as if to curry them. As with everything else, they come in Unary and Binary flavors.

The first is called BinaryCompose. It is a unary functor that uses a binary functor to evaluate two unary functors. Think of a simple range constraint checker:

 UnaryPredicate inRange = 
     new BinaryCompose( new And(),
         new UnaryPredicate() {
             public boolean is( Object arg ) {
                 return ((Long)arg).longValue() >= LONG_START; } },
         new UnaryPredicate() {
             public boolean is( Object arg ) {
                 return ((Long)arg).longValue() < LONG_END; } } );

Dbg.assert( new Long( 10 ) ) );
The name is misleading, while it does perform a binary compose, it actually instantiates unary FunctorObjects. In this example, we are still not free from writing code since we had to bind the second arguments of the range check manually as well as some other binary compositions.

If we were to really pick this expression apart, we could see that we are currying 5 FunctorObjects, all of which are logical binary operations.

  1. A := 10, LONG_START )
  2. B := 10, LONG_START )
  3. C := A, B )
  4. D := 10, LONG_END )
  5. E := C, D )

The 10 was our unary argument for the aggregate functor arg ). Using composition prevents us form having to define both Equal and NotEqual?, Greater as well as GreaterOrEqual?, and so on.

Once we go over binders we will come back to this example and see how we can eliminate all manual coding from this expression object. In other words, have a completely dynamic system to create something like the Visual Rule Builder or Visual Constraint Builder we talked about earlier. For now, here is the implementation of the compositors.

NOTE: You will need to create both LogicalFunctor as well as regular (dedicated) Function versions of these composers and binders. The following will only work with logical operators. The is method will fail for arithmetic expressions if you only implement the logical versions.

BinaryCompose Implementation

 package com.tripwire.rdifalco.ast;

public class BinaryCompose extends UnaryLogicalFunctor { /// Implementation.

private BinaryPredicate m_binary; // is(, ) private UnaryPredicate m_unaryL; private UnaryPredicate m_unaryR;

/// Interface.

/** * @param binary Any subclass of <code>BinaryPredicate</code> called in * response to is with the result of <code>unaryL</code> and * <code>unaryR</code>. * @param unaryL A subclass of <code>UnaryPredicate</code> whose result * will be used as the first argument to <code>binary</code> * when evaluating <code>is</code>. * @param unaryR A subclass of <code>UnaryPredicate</code> whose result * will be used as the second argument to <code>binary</code> * when evaluating <code>is</code>. */ public BinaryCompose( BinaryPredicate binary, UnaryPredicate unaryL, UnaryPredicate unaryR ) { m_binary = binary; m_unaryL = unaryL; m_unaryR = unaryR; }

/** * @param arg The value is sent the member <code>is</code> of the two * unary predicate sent to the constructor. * @returns A boolean value equivalent to evaluating:<p> * <code> bResult = arg ), arg ) );</code> */ final public boolean is( Object arg ) { return // NOTE: arguments must be objects!! arg ) ? Boolean.TRUE : Boolean.FALSE, arg ) ? Boolean.TRUE : Boolean.FALSE ); }

//..Some convenient factory methods

//* And two unary predicates static final BinaryCompose and( UnaryPredicate u1, UnaryPredicate u2 ) { return new BinaryCompose( new And(), u1, u2 ); }

//* Or two unary predicates static final BinaryCompose or( UnaryPredicate p1, UnaryPredicate p2 ) { return new BinaryCompose( new Or(), p1, p2 ); }

//* Create half-open range constraints static final BinaryCompose inRange( Comparable start, Comparable end ) { return BinaryCompose.and( BinaryCompose.or( new BinderSecond( new Greater(), start ), new BinderSecond( new Equals(), start ) ), new BinderSecond( new Less(), end ) ); } }

UnaryCompose Implementation

Just like Binary Compose but for two Unary Functors.

 #package com.tripwire.rdifalco.ast;

public class UnaryCompose extends UnaryLogicalFunctor { /// Implementation.

private UnaryPredicate m_pred1; private UnaryPredicate m_pred2;

/// Interface.

public UnaryCompose( UnaryPredicate pred1, UnaryPredicate pred2 ) { m_pred1 = pred1; m_pred2 = pred2; }

final public boolean is( Object arg ) { return // Convert boolean to Boolean arg ) ? Boolean.TRUE : Boolean.FALSE ); }

//..Factor Methods

//* compose negation public static UnaryCompose not( UnaryPredicate pred ) { return new UnaryCompose( new Not(), pred ); } }

AbstractBinder Implementation

This helps us build the two other binders.

 #package com.tripwire.rdifalco.ast;

public abstract class AbstractBinder extends UnaryLogicalFunctor { private BinaryPredicate m_predicate; // Binary Predicate private Object m_bound; // Bound Value


final public boolean isBound() { return m_bound != null; }

//..Bind Values

/** Rebind the l-value */ final public void bind( Object arg ) { m_bound = arg; }

/** Readapt this instance to a new binary predicate */ final public void adapt( BinaryPredicate pred ) { m_predicate = pred; }

/// Implementation

final protected boolean doIsAsFirst( Object arg ) { return arg, m_bound ); }

final protected boolean doIsAsSecond( Object arg ) { return m_bound, arg ); } }

BinderFirst Implementation

     class   BinderFirst
     extends AbstractBinder

public BinderFirst( BinaryPredicate predicate ) { this( predicate, null ); }

public BinderFirst( BinaryPredicate predicate, Object arg1 ) { super.adapt( predicate ); super.bind( arg1 ); }

//..UnaryPredicate Responsibility

/** * Evaluates the specified argument agains the bound argument using * the currently adapted predicate. */ public boolean is( Object arg2 ) { return super.doIsAsSecond( arg2 ); } }

BinderSecond Implementation

     class   BinderSecond
     extends AbstractBinder

public BinderSecond( BinaryPredicate pred ) { this( pred, null ); }

public BinderFirst( BinaryPredicate pred, Object arg2 ) { super.adapt( pred ); super.bind( arg2 ); }

//..UnaryPredicate Responsibility

/** * Evaluates the specified argument against the bound argument using * the currently adapted predicate. */ public boolean is( Object arg1 ) { return super.doIsAsFirst( arg1 ); } }

BlocksInJava: RobertDiFalco

Prev: BlocksInJavaAst Next: BlocksInJavaPuttingItTogether

EditText of this page (last edited December 17, 2006) or FindPage with title or text search