Persistent Procedure Singletons

I've been programming in Java and C++ for so long that when I need to write ABL (formerly 4GL), I'm sometimes left scratching my head.

Here's some simple Java, which makes use of a "static" ("class") method to validate someValue:

String validationMessage = com.joanju.Widget.validateSKU(someValue);

How would I write the equivalent in ABL?

ABL lacks static/class methods or functions. I suspect that such a feature couldn't be easily added to the compiler because of the lack of a class loader in the platform runtime, but I'm just guessing.

That leaves me with having to use Singletons. I don't need Singletons in the strict sense of the formal pattern - I just need them as method libraries.

There are plenty of ways to implement this sort of thing in ABL, but I had some very specific goals:

  • code that is very easy to follow
  • preprocessed code that is easy to read and follow
  • COMPILE..XREF isn't left clueless
  • scale well with lots of PP singletons
  • Callgraph can follow all calls to the functions and procedures in the PPs

I ended up having to use two of my old arch enemies: an include file and a global variable. The include file was necessary to "fake" some syntactic sugar for brevity. The global variable was necessary to implement something that looks a tiny bit like a platform's class loader.

Here's my sample test case:

{com/joanju/singletonpp.i "com/joanju/widget.p" widget}
{com/joanju/singletonpp.i "com/joanju/grommet.p" grommet}

run testSet in widget ("Hello world!").
display dynamic-function("testGet" in widget) format "x(30)".

run singletontest2.p.

The grommet.p program is empty, and I referenced it only so that I could see that my preprocessed code wasn't getting too fat with each new reference to the include file.

The singletontest2.p program just shows that the existing widget.p is found and re-used:

{com/joanju/singletonpp.i "com/joanju/widget.p" widget}

display dynamic-function("testGet" in widget) format "x(30)".

The include file uses an include guard to ensure that the global handle to a PP manager is defined and checked just once in each compile unit. The include contains the code for defining the handle and fetching the PP, giving us the much needed brevity in the main program code. The conditional RUN statement will never actually run - it is there just to make sure that Callgraph and COMPILE..XREF can do their jobs.

&if defined(com_joanju_singleton_pps) = 0 &then
  &global-define com_joanju_singleton_pps
  define new global shared variable comJoanjuSingletonPPManager as handle no-undo.
  if not valid-handle(comJoanjuSingletonPPManager) then
    run com/joanju/singletonmanager.p persistent set comJoanjuSingletonPPManager.
&endif

define variable {2} as handle no-undo.
run getSingleton in comJoanjuSingletonPPManager ("{1}", output {2}).
/* Next statement is just an xref from this program to "{1}". */
if not valid-handle({2}) then run "{1}" persistent set {2}.

Finally, the singletonmanager.p itself is dead simple:

define temp-table singleton no-undo
  field progName as character
  field progHandle as handle
  index idx1 is unique progName.

procedure getSingleton:
  define input parameter name as character no-undo.
  define output parameter newHandle as handle no-undo.
  find singleton where progName = name no-error.
  if available singleton and valid-handle(singleton.progHandle) then
    assign newHandle = singleton.progHandle.
  else do:
    run value(name) persistent set newHandle.
    if not available singleton then do:
      create singleton.
      assign singleton.progName = name.
    end.
    assign singleton.progHandle = newHandle.
  end.
end procedure.

Although there are a myriad of ways to implement function libraries and find and reference them in ABL, this method satisfies my goals:

  • All compile units referencing the PP program name can be found with a simple text search.
  • All references to the PP's functions/procedures can be found in the compile unit with a simple text search for the name of the handle.
  • Unlike with super procedures, you don't have to use particularly long PP function/procedure names to avoid name collisions or make references easy to find. (Actually, Callgraph handles supers and session supers pretty well, so finding the references wasn't such an issue, but the ability to use a simple method name like "validate" in more than one PP (name collisions) was the real consideration.)
  • There will be a COMPILE..XREF entry from the program to the PP.
  • Callgraph can resolve all of the RUN PERSISTENT SET, the RUN IN handle, and the DYNAMIC-FUNCTION(name IN handle) calls.
  • The include file does not make the program's preprocessed output fat or difficult to read.
  • Getting a handle to the PP is a one-liner (the include reference) for the developer.
  • By using a temp-table for indexed lookup for the handles, this should scale well when many dozens of PP pseudo-classes are needed.

Comments

Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Check out my procedure manager

It takes care of stuff like this and a whole lot more. It's currently over on the PEG utilities page, mostly because I haven't created a project here yet...


tamhas's picture

PPM

mostly because I haven't created a project here yet...

Other than the fact that you folks up there already had your Thanksgiving ... what better time to do it than Thanksgiving weekend!


tamhas's picture

Other thoughts about Singletons

For a solution to creating pseudo-singletons in OOABL, see Replacing A Session SuperProcedure With A Class In OOABL

Also, in a .p context, an interesting piece of code from the AutoEdge example is autoedge/server/oeri/support/extendprocedure.i which consists of:

   {1} = SESSION:FIRST-PROCEDURE.
    DO WHILE VALID-HANDLE({1}) AND 
      {1}:FILE-NAME NE "{2}":
        {1} = {1}:NEXT-SIBLING.
    END.
    IF NOT VALID-HANDLE({1}) THEN
        RUN support/{2} PERSISTENT SET {1}.
    THIS-PROCEDURE:ADD-SUPER-PROCEDURE({1}).

where the first argument is a procedure handle and the second is the name of the procedure. This doesn't do as much management as John's code does, but it is a simple way to run one and only one copy of a super-procedure.


john's picture

procedure lookup and scaling

That exact technique is why I made the comment about using an indexed lookup lookup for the handles, so that it would scale well with dozens of persistent procedures. It would be interesting to hear if anybody has ever put a stopwatch to that method vs. a lookup in a temp-table. I can't imagine that dozens of string comparisons comes without a price. It's not unusual for a RUN statement to find itself in a tight loop... :)


Performance is good

My procedure manager's running in a production application, and there's no noticeable delay when starting all the procedures (as well as figuring out their dependencies and starting new instances as required).

The big plus with the procedure manager compared to traversing a list of procedure handles is it gives the developer a lot more flexibility in terms of what kind of persistent objects one creates, as well as taking away all the management / cleanup problems.


tamhas's picture

Relative Performance

I don't see the issue as being performance as much as it is the level of management. Your approach or any other persistent procedure manager seeks to provide a conscious level of management to the PPs instantiated in a session. The simple code snippet illustrates a way to not use a manager, but to still instantiate only a single instance per session. This is not something that one would have within a tight loop because it is start of program code to obtain a handle. Thence forth, one does runs in handle and that would be the run which might appear in the tight loop.

Of course, I wouldn't use either one any more ... I'd use the OO version!


john's picture

RUN in tight loop

I think you misunderstood. Yes program A would obtain the handle just once at its start. It's the RUN A.p statement(s) that I'm concerned about. Or the calls which lead to those calls...


tamhas's picture

When?

If one is running this code only once at initialization, I don't see how the performance difference is relevant.