Home

News (12/11/08)new

Start Here
Getting Started
Downloads


Documentation

Tutorials (12/07/08)new
Tablets Structure
Deployment
Web Studio
OpenL Basics
Constrainernew

Change Log

References
LGPL License


Motivational Reading

OpenL Apologia



SourceForge.net Logo

OpenL Programming Language Framework

As we all know, Business Rules consist of rules. Each Rule has Condition and Action. Condition is a boolean expression (the one that returns true or false). Action can be any sequence(usually simple) of programming statements. What kind of language is most suitable for this task?

Let's take a look at the expression, that probably is as ubiquitous in any BR doc as "if customer's level is GOLD "

driver.age < 25

From the semantic perspective, the expression intents to define the relationship between some value defined by 'driver.age' expression and literal '25'. One might guess, that the English semantic of the statement could be any of if age of the driver is less than 25 years or select drivers who are younger than 25 years old or some other.

From the programming language perspective, the semantic part is irrelevant, the statement should only be

  • a valid statement in the language grammar
  • a statement should be correct from the type-checking point of view
  • if language is compiled, the valid binary code or some other results of compiling (for example bytecode, or even code in some other target language might also be considered as possible results of the compiling) should be produced from the statement
  • some kind of runtime system, interpreter or Virtual Machine should be able to execute (interpret) this statement's compiled code and produce a resulting object

    OpenL Grammars

    When OpenL parser parses an OpenL expression it produces a Syntax Tree. Each Tree Node has a node type, a literal value, a reference to the source code for displaying errors and debugging, and also may contain children nodes. This is similar to what other parsers do, with one notable exception - the OpenL Grammar is not hard-coded, it can be configured, and different can be used. Having said this, we also must admit that for all the practical purposes, as of today, we distribute only the following grammars implemented in OpenL: org.openl.j - based on "classic" Java 1.3 grammar (no templates and exception handling) and org.openl.bex - which is basically org.openl.j grammar with "business natural language" extensions. The latter is used by default in OpenL Tablets business rules product.

    We also have experimental org.openl.n3 grammar and we may add org.openl.sql grammar in the future.

    The Syntax Tree produced by the org.openl.j grammar for the expression we started with will look like this:

            <
           / \
          .   25
         / \
    driver  age
      	
    
    The node types of the nodes are
  • op.binary.lt for '<'
  • literal.integer for '25'
  • chain for '.'
  • identifier for 'driver'
  • identifier for 'age'

    Node type names are significant, as we will see later, but at this point they look rather like random names.

    NOTE. It is also important to recognize that the Grammar we use in org.openl.j is similar not only to Java but to any other language in C/C++/Java/C# family. This makes OpenL easily learned and accepted by the huge pool of available Java/Cxx programmers and adds to it's strength. The proliferation of new languages like Ruby, Groovy, multiple proprietary languages used in different Business Rules Engines, CEP Engines etc., introduced not only the new semantics to the programming community, but also a bunch of new grammars that make the acceptance of the new technologies much harder.

    We at OpenL work day and night to stay as close to the Java syntax as possible to make sure that the "entities would not be multiplied beyond necessity". Let's keep the world's linguistic entropy down, folks.

    Context, Variables and Types

    After the Syntax Tree had been created, the next stage of the compilation process, or Binding, binds syntax nodes to it's semantic definitions. At this stage, OpenL uses specific Binders for each node type. The modular structure of OpenL allows to define custom Binders for each node type. Once syntax node had been bound into Bound Node, it has been assigned a type, making the process type-safe.

    Most of the time, the standard Java approach is used to assign type to the variable - it should be defined somewhere in the context of the OpenL framework. Typical examples include:

  • Method parameter
  • Local Variable
  • Member of surrounding class (in case of OpenL it is usually the implementation of IOpenClass called Module)
  • External types accessed as static, mostly Java classes that are imported into OpenL
  • Fields and Methods in binding context - this is a feature that does not exist in Java; OpenL allows programmatically add custom types, fields and methods into Binding Context; for different examples of how it could be done you need to take a good look at the source code of OpenLBuilder classes in different packages. For example, org.openl.j automatically imports all the classes from the java.util in addition to the standard java.lang package. Since version 5.1.1 java.math is also being imported automatically

    OpenL Type System

    Everybody knows that Java is a type-safe language. But it's type-safety ends when Java has to deal with types that lie outside of Java type system - like database tables, http requests or XML files. There are two approaches to deal with those "external" types - use API or use code-generation. API approach is inherently not type-safe, it treats attribute as literal strings, therefore even spelling errors will be visible only in runtime. Another problem with API - it is well, API-specific, so unless the standard API exists, your program becomes dependent on the particular API. The approach with code-generation is better, but it also introduce an extra building step and is dependent on particular generator, especially the part where names and name spaces are converted into Java names and packages. Often, the generators introduce dependencies with runtime libraries that also affect the portability of the code. Finally, generators usually require full conversion from external data into Java objects that may incur an unnecessary performance penalty in the case where you need to access only a few attributes. OpenL Open Type system gives you the simple way to add new types into OpenL language, all you need is to define a class object that implements IOpenClass interface and add it to OpenL type system. The implementations can vary, but access to object's attributes and methods will have the same syntax and will provide the same type-checking in all OpenL code throughout your application.

    OpenL Tablets as OpenL Type extension

    OpenL Tablets is built on top of OpenL type system, and this allows it to integrate naturally into any Java or OpenL environment. Using OpenL methodology, Decision Tables become Methods, and Data Tables become Fields. The similar conversion happens to all the other project artefacts. It allows for easy modular access to any project's component through Java or OpenL code. An OpenL Tablets project itself becomes a "class" and easy Java access to it is provided through a generated JavaWrapper class.

    Operators

    Operators are just another methods with priorities defined by the Grammar. OpenL has 2 major types of operators: unary and binary. In addition, there are other operator types used in special cases. Here is a complete list of OpenL operators used in org.openl.j Grammar - the one that is used by default in OpenL Tablets product.

    When we say that OpenL has a modular structure, we not only refer to the fact that OpenL has configurable, high-level separate components like Parser and Binder, it as also, that each node type can have it's own NodeBinder. At the same time, we can assign the single NodeBinder to a group of operators, like we do in the case of the prefix op.binary.

    NOTE.(op.binary.or '||' and op.binary.and '&&' have separate NodeBinders to provide short-circuiting for boolean operands). For all other binary operators OpenL uses a simple algorithm, based on the operator's node type name. For example, if the node type is 'op.binary.add', the algorithm looks for the method named 'add()' in the following order:

    • Tx add(T1 p1, T2 p2) in the namespace org.openl.operators in the BindingContext
    • public Tx T1.add(T2 p2) in T1
    • static public Tx T1.add(T1 p1, T2 p2) in T1
    • static public Tx T2.add(T1 p1, T2 p2) in T2

    The found method is then being executed in the runtime. So, if you need to override binary operator t1 OP t2 (where t1, t2 are objects of classes T1, T2 ), you need to do the following steps:

    1. Check the Operators Table and find the operator's type name.
    2. The last part of the type name will give you the name of the method that you need to implement
    3. Now you have the following options for implementing operators:
      • put it into some class YourCustomOperators as the static method and register the class as the library in org.openl.operators namespace (see OpenLBuilder code for more details).
      • implement as method in T1: public Tx name(T2 p2)
      • implement as method in T1: static public Tx name(T1 p1,T2 p2)
      • implement as method in T2: static public Tx name(T1 p1,T2 p2)

    Usually, if T1 and T2 are different, you need to define both OP(T1, T2) and OP(T2, T1), unless you can rely on autocast() operator or Binary Operators' Semantic Map. Autocast can help you skip implementation when you already have an operator implemented for the autocasted type. For example, if you have OP(T1, double), you don't have to implement OP(T1, int), because int is autocasted to double. You may incur some performance penalty by doing this though. Binary Operator Semantic Map is described next.

    Binary Operators' Semantic Map

    Since the version 5.1.1 there is one convenience feature that we call Operator Semantic Map. It makes implementing of some of the operators easier by describing properties( symmetrical and inverse) for some operators.

    Unary Operators

    For unary operators, the same method resolution algorithm is being applied, the only difference is that there is only one parameter to deal with.

    Cast Operators

    Cast Operators in general correspond to Java guidelines and come in 2 types: cast and autocast. T2 autocast(T1 from, T2 to) methods used to overload implicit cast operators (like from int to long, so that actually no cast operators are required in code), T2 cast(T1 from, T2 to) methods are used with explicit cast operators.

    NOTEIt is important to remember that while both cast() and autocast() methods require 2 parameters, only T1 from parameter will be actually used. The second parameter is needed to avoid ambiguity in Java method resolution

    Appendix A. The List of org.openl.j Operators

    In the order of priority:

    Assignment operators
    =
    op.assign
    +=
    op.assign.add
    -=
    op.assign.subtract
    *=
    op.assign.multiply
    /=
    op.assign.divide
    %=
    op.assign.rem
    &=
    op.assign.bitand
    |=
    op.assign.bitor
    ^=
    op.assign.bitxor
    Conditional Ternary
    ? :
    op.ternary.qmark
    Implication
    ->
    op.binary.impl (*)
    Boolean OR
    || or "or"
    op.binary.or
    Boolean AND
    && or "and"
    op.binary.and
    Bitwise OR
    |
    op.binary.bitor
    Bitwise XOR
    ^
    op.binary.bitxor
    Bitwise AND
    &
    op.binary.bitand
    Equality
    ==
    op.binary.eq
    !=
    op.binary.ne
    Relational
    <
    op.binary.lt
    >
    op.binary.gt
    <=
    op.binary.le
    >=
    op.binary.ge
    Bitwise Shift
    <<
    op.binary.lshift
    >>
    op.binary.rshift
    >>>
    op.binary.rshiftu
    Additive
    +
    op.binary.add
    -
    op.binary.subtract
    Multiplicative
    *
    op.binary.multiply
    -
    op.binary.divide
    %
    op.binary.rem
    Power
    **
    op.binary.pow (*)
    Unary Operators
    +
    op.unary.positive
    -
    op.unary.negative
    ++x
    op.prefix.inc
    --x
    op.prefix.dec
    x++
    op.suffix.inc
    x++
    op.suffix.dec
    !
    op.unary.not
    ~
    op.unary.bitnot
    (cast)
    type.cast
    |x|
    op.unary.abs (*)
    (*) Operators do not exist in Java Standard, only in org.openl.j, but yoo can use and overload them at will

    Appendix B. The List of org.openl.j Operator Properties

    Symmetrical

    eq(T1,T2) <=> eq(T2, T1)
    add(T1,T2) <=> add(T2, T1)
    

    Inverse

    le(T1,T2) <=> gt(T2, T1)
    lt(T1,T2) <=> ge(T2, T1)
    ge(T1,T2) <=> lt(T2, T1)
    gt(T1,T2) <=> le(T2, T1)