Declarative programming is a programming paradigm that create a logic for computation with little control flow statements. It uses domain declaration statements to describe a problem. We use declarative programming to create smart workflows and database models.

Workflow Concept

A workflow is a program or procedure that is organized in steps. A step has name and description. Each step define a sub-region that can contain one or several statements.  The sequence of steps end with {recover|finalize} or the end of section.

A step contains multiple statements. If a step fail, the program can continue with next step or interrupt activity and return. This can be used to organize large test suites or data pipelines.

Keyword “this”

We can use “this” keyword to represent a module section: In functions when we wish to refer to parent of a member we can use: “parent” or “program” or “module”. We use parent when the section is nested for refering to shadowed variables defined in parent of a section. We can associate a description to any named section using notation “this.description” in local declarations.

Example:

procedure main is
 this.description="Program Description";
begin
 pass;
end procedure;

Workflow Syntax

procedure <name>() is
  <declaration region>
begin
  <initialization>
step <step_name>:<description> is
  <step_implementation>;
  when <step_condition> then [pass | fail | jump | return | raise];
step <step_name>:<description> is
  <step_implementation>;
  [when <step_condition> then raise(code,"error message");]
  ...
[recover]
  <exception_region>
[finalize]
  <output_report>;
end procedure;

Step region

Step is an executable sub-region that use keyword “step”. It can have a name and a description. The step name can be a normal identifier. It can be as simple as {a1,a2,a3} or {case1, case2} or {step_10,step_20}. It is a good practice to use <prefix>nnn and count the steps from 100 to 900 so that steps can be reordered. You can count using 10 interval like 110…990. So you can have space to insert other steps.

The <description> of the step is a string. This describe very shortly what the step does. Description of the step is stored in a local structure of type Trace that contains step name, description and execution status. This structure can be used for reporting at the end of the procedure in the finalization region. The structure has always reserved name: trace. It is populated automatically after step execution.

Current step

The current step is identified using keyword step and can be used with dot operator. For example current step description is: step.description; This variable is available in recovery section so we can create exception handlers using current step status and name. We can create step references using keyword “@Step”.

let 
  s: @Step; -- step reference
do
  s?=step; -- assign current step to a reference
done;

Early termination

In the step region programmer can make decisions to verify if the step has pass or has failed. The execution of steps can be interrupted by an exception and resumed by a recover statement. The “resume” keyword will continue execution of steps with the next step. The exit will continue execution with finalization region.

when <condition> then [ pass | fail | skip | return | raise ];

if <condition> then 
   pass | fail | skip | return | raise;
end if;
  • pass; => continue with next statement
  • fail; => continue with next statement
  • skip; => skip current step and continue with next step
  • skip to <step_name>; => skip current step and continue with <step_name>
  • return; => stop sequences of steps and execute finalization region
  • raise(code,”message”); => stop sequences of steps and execute recovery region

Note: It is not possible to jump to a previous step that was already executed. This is to avoid spaghetti code that is considered a bad practice. If a step can be executed multiple times this should be converted into a function or method. Then a repetition block can be used (loop).

Step status

A step can have status: {pass, fail, error}.

  • default status is “none”
  • pass; => record status= pass
  • fail; => record status= fail
  • panic => record status= error
  • raise; => record status= error

Using these clauses developer can record status in workflow trace. If an exception is raised during one step then execution of next step is interrupted. Program continue with recover region, finalization region then return execution to the caller.

Workflow Trace

Any workflow has a variable “trace” predefined. This variable is a collection of records that can be used to display workflow status. This variable can be used in the finalization region to display the report or save content into a file.

Workflow library

We define Step record type in standard library Workflow.

type
   Step:Record of (
     name:String,        {*name of the step *}
     status:String,      {*pass/fail/error/exit/skip*}
     description:String, {*step description*}
     start_date:Date,    {*the start date*}
     start_time:Time,    {*the start time*}
     end_time:Time,      {*the end time*}
     err_message:String, {*exception message*} 
     err_code:String     {*exception code*} 
   );

-- automatic variable defined by the compiler
-- n is determined automatically by the compiler
variable
  trace: Vector(n) of Step;

Workflow current step

The current step is identified using keyword step. We can use _step_ with dot operator or using with… do statement to avoid qualifier repetition. This variable is automatically created by the compiler and populated. Current step is also found in _trace_ report.

print(step.name);
with step do
  print name;
  print description;
  print status;
end do;

How to use workflow?

procedure test_workflow is
  this.description="procedure test_workflow description "; 
  v=5:Integer;
begin
step a:"first step" is
  when v<5 then pass; 
step b:"second step" is
  when v>5 then fail;
step c:"third step" is
  when v==5 then exit; 
step d:"forth step" is
  pass; --> this will never execute
recover
  if step.name in ["a","b"] then
     resume;
  else
     fail;
  end if;
finalize
  for s in trace loop
    print("#s:#s has #s;" <- (s.name, s.description, s.status));
  end loop; 
end program;
a:first step has pass;
b:second step has fail; c:third step has exit;

What to do with the workflow trace?

A trace can be used to create a report file or record the report into a database or generate a web page. Whatever the business case requires. Developers can create beautiful HTML reports using templates or can even create XML reports for test automation.

Each module has it’s own trace report. We can have a common trace report define in main program even if the main program is not using steps. If a global variable $trace is defined the module trace report can be appended to global report.

Example:

#type:workflow

-- This is a program that call one workflow 2 times
import 
  from workflow use all;-- avoid qualifiers 

-- this is a subroutine with workflow 
procedure work(p:Integer) is 
  this.description="a simple workflow";
begin 
step s1 is 
  print("p^2=" & p^2); 
step s2 is 
  print("p^3=" & p^3); 
finalize 
  -- transfer trace report to file
  trace.save("my_work.log"); 
end procedure;

-- main program workflow
procedure main is
begin
  work_module(2);
  work_module(3);
end program;

Workflow usability

Workflows can be used to create test cases or large batch processes. One program can have hundreds of workflows organized in modules. The main program control the execution of workflows. In Level-2 we define Job section that can be used to run several workflows in parallel.

See also: Level Specification