Tuesday, December 28, 2010

Using JBoss Rules (Drools) in Clojure

On December 11th, I posted a demonstration of how developers could interact with the JBoss Rules framework in Scala (not only to instantiate and run the rules engine, but also to provide model objects for consideration in the rules process). This week, I want to demonstrate the same principles, but with the JVM-based Lisp variant, Clojure (http://clojure.org/).

The Clojure programming language was created by Rich Hickey in 2007 in an effort to bring a capable functional language to the JVM.  Hickey's Lisp implementation was design to address two issues specifically.  First, Hickey wanted a language that was completely interoperable with Java (Clojure uses Java primitives and compiles to byte code).  Second, Clojure was designed to address problems commonly encountered in concurrent programming (via actor model and software transactional memory).  For a more detailed description of the Clojure language, I suggest you visit the language's website. [1]

For those unfamiliar with the JBoss Rules framework (Drools), I highly suggest you visit the project's website:  http://www.jboss.org/drools.  In short, Drools enables developers to abstract business logic from their "middle tier" (Business layer).  The Drools framework is a rules engine, business process orchestrator and event processor all rolled into one.  For today's demonstration, however, we will only concern ourselves with the rules engine aspect of the framework, Drools Expert.

For today's demonstration, we will mirror the last post's functionality of evaluating Temperature objects.  As a reminder,  if the temperature is greater than 85 degrees, a "HEAT WARNING" should be set. Alternatively, if the temperature is less than 32 degrees, a "FREEZE WARNING" will be declared.

If you read the previous article, you will probably notice that this is the same rules file used for the Scala demonstration:

package com.berico.rules
 
import com.berico.model.weather.Temperature

rule "Too Hot"
   dialect "mvel"
   when
      temp : Temperature( value > 85 )
   then
      System.out.println(
        temp.value.toString() + " F is too hot."
        + " Declare HEAT WARNING!"); 
end

rule "Too Cold"
   dialect "mvel"
   when
      temp : Temperature( value < 32 )
   then
      System.out.println(
         temp.value.toString() + " F is too cold."
         + " Declare FREEZE WARNING!"); 
end

Clojure is probably one of the best languages (next to Groovy) in terms of interoperability with Java; this is especially surprising considering how dissimilar both languages are to each other. A program written completely in Clojure doesn't really need classes or interfaces. To interoperate with other JVM languages, Clojure has a method for class and interface generation [2]. The following code is a fully-compatible class written in Clojure.

*Note: lines starting with double semi-colons ";;" are comments (sorry syntax highlighting is not working at the moment).

(ns com.berico.model.weather.Temperature
  (:gen-class
   :init init

   ;; Define the method `getValue` 
   ;; which will be accessible to Java
   :methods [[getValue [] double]]

   ;; The double brackets indicate a default 
   ;; (parameterless) constructor
   :constructors {[double] []}

   ;; Store instance variables in 
   ;; the state variable
   :state state))

;; Constructor (accepts a double `value`)
(defn -init [value]

  ;; we are actually going to store reference to the
  ;; value as a key-value pair in the variable `state`
  [[] (ref {:value value})])

;; Get the value of the Temperature object 
;; (e.g. temperature in Fahrenheit)
(defn -getValue [this]

  ;; The get function allows us to pull the 
  ;; key `value` from the state variable
  (get @(. this state) :value))

Just like in Java, we need to declare our namespace (package) and class imports that exist in this Clojure file's scope. Note the use of vectors for the org.drools and org.drools.builder namespaces; we can use this 'syntax sugar' to include multiple classes from a namespace on an import (without the need for 'greedy' inclusion).

(ns com.berico.rules.RuleRunner
  (:gen-class
   :require ['[
     [org.drools KnowledgeBase 
                 KnowledgeBaseFactory]
     [org.drools.builder KnowledgeBuilder 
                         KnowledgeBuilderError 
                         KnowledgeBuilderErrors 
                         KnowledgeBuilderFactory]
      org.drools.builder.ResourceType 
      org.drools.io.ResourceFactory 
      org.drools.runtime.StatefulKnowledgeSession]]))

The following is a function to construct the KnowledgeSession we will need for inserting facts and running the Weather rules.

* Clojure does not support the "aliasing" of Java classes [yet] (though, it is possible for Clojure namespaces).  This is the reason why we are using fully qualified class names in the code.


;; Construct a Knowledge Session 
;; from the 'WeatherRules.drl' definition.
(defn build-knowledge-session [] 

  ;; Create the KnowledgeBuilder object
  (def knowledge-builder 
    (org.drools.builder.KnowledgeBuilderFactory/newKnowledgeBuilder))

  ;; Adding the Weather Rules definition 
  ;; to the KnowledgeBuilder
  (.add knowledge-builder
    (org.drools.io.ResourceFactory/newClassPathResource 
       "WeatherRules.drl")
     org.drools.builder.ResourceType/DRL)

  ;; Creating the KnowledgeBase
  (def knowledge-base 
    (org.drools.KnowledgeBaseFactory/newKnowledgeBase))

  ;; Adding Knowledge Packages to the KnowledgeBase
  (.addKnowledgePackages knowledge-base 
    (. knowledge-builder getKnowledgePackages))

  ;; Create a Stateful Knowledge Session 
  ;; and return it to the calling function
  (. knowledge-base newStatefulKnowledgeSession))

And finally, the entry point of our application. We begin by creating the KnowledgeSession. The next two statements create Temperature objects, initializing their values to 100 and 20 degrees Fahrenheit, respectively. Finally, using the "doto" method, we call 3 functions on the KnowledgeSession in sequence, inserting the Temperatures as facts and finally firing all rules.

;; Start the Application
(defn app-start []

    ;; Create the Knowledge Session
    (def knowledge-session (build-knowledge-session))

    ;; Create a Temperature fact
    (def shouldBeTooHot 
      (new com.berico.model.weather.Temperature 100))

    ;; Create another Temperature fact
    (def shouldBeTooCold 
      (new com.berico.model.weather.Temperature 20))

    ;; Insert the facts & fire all rules
    (doto knowledge-session 
      (.insert shouldBeTooHot) 
      (.insert shouldBeTooCold) 
      (.fireAllRules)))

I'm running this application using the Clojure REPL. You initiate the application using the "app-start" method. Here's the output from the console:

Clojure 1.2.0
1:1 user=> #<Namespace com.berico.rules.RuleRunner>
1:2 com.berico.rules.RuleRunner=> (app-start)
20.0 F is too cold. Declare FREEZE WARNING!
100.0 F is too hot. Declare HEAT WARNING!
#<StatefulKnowledgeSessionImpl 
    org.drools.impl.StatefulKnowledgeSessionImpl@3ab28980>
1:3 com.berico.rules.RuleRunner=>

As you can see, developers are not limited to Java when using the JBoss Rules framework.  I hope you found this post helpful, but more importantly encouraging (you are not limited to Java on the JVM!). 

Finally, I wanted to thank Mark and the rest of the Drools team for recognizing my Dec 11th post.  You guys doubled my all time page views in a day (and it was a holiday)!  More importantly, thanks for your extremely impressive framework.

Richard Clayton 

References:

[1]. http://en.wikipedia.org/wiki/Clojure
[2]. http://clojure.org/compilation

Resources:

I personally own, and recommend, the following books on Clojure and JBoss Rules:

1 comment:

  1. Hi Can you mail me complete source code this one to ganeshneelekani@gmail.com

    Thanks

    ReplyDelete

Note: Only a member of this blog may post a comment.