OmniMark Programming Principles

www.serverside.com.au

Chapter 2
Basic Principles of OmniMark Programming


[Back to the General Index] [Back to the Chapter Summary]

This chapter covers:

Topic Index

2.1: Rule-based stream processing

OmniMark programs are quite different to those in most other languages. In languages like Pascal, BASIC and C (structured-design languages) the programmer creates a main module to control the overall work and farms out specific tasks to sub-modules (functions) which return control to the main module when they terminate. In Java, C++ and SmallTalk (Object-Oriented Languages), the programmer creates objects each of which is a capsule containing some data and some associated functions. The objects contribute to an overall problem solution by sending messages to each other and a main module is mostly used just to start the application. All these languages are procedural in that either a clearly defined sequence of processing is apparent in a solution to a particular problem or events that need certain processing are trapped by a controller, farmed out to various parts of the software, eventually to return to the controller. Event-driven software written in these languages is quite common with the events being triggered when certain processes need doing.

The majority of OmniMark programming is declarative, which means that the language is made up of rules for dealing with events which are triggered by data coming into the program from a stream. The main power of OmniMark is dealing with data events and in later chapters I will deal specifically with this issue.

In this chapter I only want to expose some simple fundamentals of OmniMark's program structure.

Topic List

2.2: Writing your first program

To write an OmniMark program you type instructions into a text editor. You can use any editor you like for this - my favourite on Windows machines is PFE (Programmer's File Editor) and in UNIX, vi. If you are going to use the OmniMark IDE, an editor is provided and has the advantage of rendering the reserved words and structures of the language in various colours.

As a starting point, launch either the OmniMark IDE or your favourite text editor and write the following program into it. Don't type the line numbers.

[Code Sample: C02T02a.xom]

001  ; My first OmniMark program
002  ; [put your own name here]
003  ; [put today's date here]
004  
005  process
006    output "My first OmniMark program%n"

Save the source code file onto your local file system in any suitable directory using the filename 'first.xom'. You can save the file from within the IDE by using the command 'Save File As' from the 'File' menu.

The program starts with three lines of comments. It is a good idea to place preliminary comments in all programs in all languages. In OmniMark the ';' (semicolon) character is used to start a comment and the comment extends to the end of its line - just like the '//', characters in Java or C++.

There is only one rule in the program, and it simply indicates that 'when the program reaches this rule, the following action should be executed'.

Topic List

2.3: Executing OmniMark programs

2.3.1 Using the IDE

To execute the program in the IDE, choose 'Start' from the 'Debug' menu, then choose 'Continue' from the same menu. You can also do the same thing using buttons on the IDE toolbar. The output from the program should appear in the pane at the bottom of the IDE (the log window). A snapshot of what my IDE looks like after doing this is shown below:

2.3.2 Using the command line

To run the program from the command line of your system's console, you need to call the OmniMark executable and tell it the name of your program. This is done by typing

  omnimark -s first.xom

which is actually the same command you can see above the output in the log window of the screen shot above. The option '-s' is used to prefix the source file (the program source code). A more verbose alternative is

  omnimark -source first.xom

The output should appear on your console window (your standard output device). To avoid the extra information about OmniMark, its version and copyright notice appearing on standard output, you can use the alternative command

  omnimark -sb first.xom

where the option '-sb' combines the 'source' option and also a 'brief' option.

Topic List

2.4: The process rules

The sample program above has one rule, a 'process' rule. You can have as many of these process rules as you like in a program, for example:

[Code Sample: C02T04a.xom]

001  ; Several process rules
002  
003  process
004    output "Do this first%n"
005  
006  process
007    output "Do this second%n"
008  
009  process
010    output "Do this last%n"

Later you will see that these can be qualified so that some of them fire and some don't.

2.4.1 The process-start rule

A special rule is available which is certain to fire before all other rules. It is 'process-start'. It's important to realise right now that this rule does not have to be the first rule located in the source code, it's name alone assures that it is fired first, as shown here where the process-start rule actually appears last in the source code:

[Code Sample: C02T04b.xom]

001  ; A process start rule
002  
003  process
004    output "Do this first%n"
005  
006  process
007    output "Do this second%n"
008  
009  process
010    output "Do this last%n"
011  
012  process-start
013    output "I am in the start rule!%n"

The output from this program is:

I am in the start rule!
Do this first
Do this second
Do this last

2.4.2 The process-end rule

In an identical way, the special 'process-end' rule fires after all others, no matter where it appears in the code, demonstrated with this program:

[Code Sample: C02T04c.xom]

001  ; Process start and end rules
002  
003  process-end
004    output "I am in the end rule%n"
005  
006  process
007    output "Do this first%n"
008  
009  process
010    output "Do this second%n"
011  
012  process
013    output "Do this last%n"
014  
015  process-start
016    output "I am in the start rule!%n"

for which the output is:

I am in the start rule!
Do this first
Do this second
Do this last
I am in the end rule

The principle here is that OmniMark programs consist of rules of which the 'process' rule is the most general. These process rules do not have to fire in the sequence they occur in the source code, instead they fire in the order

It is legal to have more than one process-start rule or process-end rule, although it's a little illogical. If there are more than one of the same rule they fire in the order they appear. It is even legal to have no rules at all, but in this case there can be no actions performed by the program. If you write a program with actions but no rules you will get an OmniMark error message as shown below:

[Code Sample: C02T04d.xom]

001  ; Actions but no rules
002  
003  output "I am alone!%n"

omnimark --
OmniMark Error 3023 on line 3 in file C02T04d.xom:
Syntax Error.
Expecting a rule, but received 'OUTPUT'.

There was 1 error detected.

Topic List

2.5: Writing output

As you have seen in the samples above, the sink of the 'output' action is the standard output device (the screen if you use the command line, or the log window if you use the IDE). The output action requires a string of zero or more characters, and you have probably guessed that the special character sequence '%n' represents a newline code.

To have the output from your OmniMark program redirected and saved in a file on your file system, you only need to specify the name of an output file in the command line, like this:

  omnimark -sb program.xom -of outfile.dat

which executes a source file called 'program.xom' and redirects its output to a file called 'outfile.dat'. Unfortunately, using the '-of' option is only allowed when you call OmniMark from the command line, it is not available in the IDE. To capture your output from the IDE, you have to right-click on the log window and choose 'Copy' from the pop-up menu. This copies the contents of the log window onto the clipboard which you must then manually paste into the editor in order to save it.

OmniMark provides no checking for existing files when you use the '-of' option. The output is written over the top of any existing file or into a new file if there is no existing one with the given filename. It is possible (and easy) to write statements in your source code which allow you to write output onto any external file, to append output to an existing file or to check wether or not a file exists. IDE users can use this technique when necessary to get the same facility as command line users.

Topic List

2.6: Simple variables

No programming language would be much use without variables. There are three basic variable data types.

Some other (more complex) types will be introduced later in this booklet.

The scope of variables can be either global (available in all rules) or local to a particular rule. Variables must be declared (given a name and type) before they can be used and local variables of a given rule must be declared before any actions in that rule.

2.6.1 Stream variables

The following program uses a global stream variable:

[Code Sample: C02T06a.xom]

001  ; Using a global stream variable
002  
003  global stream shortMessage
004  
005  process
006    set shortMessage to "Hi there"
007    output shortMessage join "%n"
008    set shortMessage to shortMessage join ", how are you?"
009    output shortMessage join "%n"
010  
011  process
012    output "The message is: %g(shortMessage)%n"

The stream variable 'shortMessage' is declared on line 3 and used in both process rules. Line 4 is a direct assignment of some literal characters to the variable and the value of the variable, joined together with a newline, is output on line 7.

The 'join' keyword concatenates two streams and has been used on line 8 to append more characters onto the existing value. In all these cases the output action is given just the name of the variable and outputs the characters in it.

In line 12 we see that the same variable 'shortMessage' is available in other rules - because it has global scope. The output action in line 12 shows that stream variables can be included in literal streams by using the special converter symbol '%g' followed by the variable name in brackets. The '%g' is the correct format modifier to use with stream variables when their value is literally needed.

As you should be able to anticipate, the output from the program is

Hi there
Hi there, how are you?
The message is: Hi there, how are you?

2.6.2 Counter variables

Counter variables only hold whole numbers (like an 'int' in C, C++ and Java), and there are several easy ways to modify them. The following program creates a global counter variable called 'someNum' with an initial value and then modifies its value and displays it several times.

[Code Sample: C02T06b.xom]

001  ; Using a global counter variable
002  
003  global counter someNum initial {12}
004  
005  process
006    output "%d(someNum)%n"
007    increment someNum
008    output "The number is now: %d(someNum)%n"
009    increment someNum by 7
010    output "The number is now up to: %d(someNum)%n"
011    decrement someNum
012    output "The number is now down to: %d(someNum)%n"
013    decrement someNum by 25
014    output "The number is now down to: %d(someNum)%n"
015  
016  process
017    set someNum to someNum * 6
018    output "The number is now: %d(someNum)%n"
019    set someNum to someNum / 5
020    output "The number is now: %d(someNum)%n"

On line 6, we use the format modifier '%d' to convert the value of the counter variable into a stream of characters so that the output action can process it. It is a principle of OmniMark programming that the output action requires stream data.

Lines 7, 9, 11 and 13 show how counters can be incremented and decremented.

The second process rule shows that simple arithmetic can be performed on counters - note however that if the result of a division operation contains a fractional part, that fractional part is lost since the result must be stored in a whole number counter. The output from the above program is:

12
The number is now: 13
The number is now up to: 20
The number is now down to: 19
The number is now down to: -6
The number is now: -36
The number is now: -7

2.6.3 Switch variables

OmniMark uses the type 'switch' for boolean variables which can have one of just two values, true or false. These are usually used to encapsulate conditions which are tested by selection statements and are of limited use by themselves. The sample program below declares, initialises and modifies a switch variable just to demonstrate the syntax but does not use the variable for any serious purpose. Switch variables can be used in selection statements which are described in the next section.

[Code Sample: C02T06c.xom]

001  ; A switch variable in use
002  
003  global switch itsRaining initial {true}
004  
005  process
006    set itsRaining to false
007    activate itsRaining
008    output "You will get wet%n" when itsRaining
009    output "You can play outdoors%n" unless itsRaining
010    deactivate itsRaining
011    output "You will get wet%n" when itsRaining
012    output "You can play outdoors%n" unless itsRaining

You can assign a true or false value to a switch variable with the 'set' action as shown in line 4 or use 'activate' to assign true (line 5) or 'deactivate' to assign false (line 8). The actions on lines 6 and 9 are only executed if the switch variable holds true and those on lines 7 and 10 are only executed if the variable holds false.

Topic List

2.7: Selection and Iteration

These basic control structures obey similar logic to those you might be familiar with in other languages. The syntax of OmniMark selections and iteration structures are different by quite easy to remember.

2.7.1 Binary Selection

A binary selection structure is formed with a block of code starting with the keyword 'do' and ending with the keyword 'done'. After the word 'do' there is a condition. The program below checks if the value of a counter is greater than 17 and if so, outputs a message:

[Code Sample: C02T07a.xom]

001  ; a one-armed selection
002  
003  global counter age
004  
005  process
006    set age to 16
007    output "Your age is %d(age)%n"
008    do when age > 17
009      output "You are allowed to vote%n"
010    done
011    output "Have a nice day.%n"

The following program uses the optional 'else' keyword so that a specific output can be given when the condition is false:

[Code Sample: C02T07b.xom]

001  ; a two-armed selection
002  
003  global counter age
004  
005  process
006    set age to 16
007    output "Your age is %d(age)%n"
008    do when age > 17
009      output "You are allowed to vote%n"
010    else
011      output "You are too young to vote%n"
012    done
013    output "Have a nice day.%n"

When only one action is being guarded by a condition, it is convenient to check the condition as part of the action. The next program does exactly what the one above does but uses guarded actions.

[Code Sample: C02T07c.xom]

001  ; conditional actions
002  
003  global counter age
004  
005  process
006    set age to 23
007    output "Your age is %d(age)%n"
008    output "You are allowed to vote%n" when age > 17
009    output "You are too young to vote%n" unless age > 17
010    output "Have a nice day.%n"

2.7.2 Iteration

OmniMark programs rarely need counter-controlled iteration (loops that iterate a fixed number of times). The 'repeat' and 'again' block is almost always used to traverse an OmniMark array or SGML tree, and, at this stage, we have not discussed this type of processing. Consequently, the example program below is simplistic. It contains a loop which output the numbers from 1 to 10 and indicates whether each one is even or odd. The decision about even or odd values is another example of an OmniMark selection control. Note also that the counter variable 'num' has been declared as local in this example.

[Code Sample: C02T07d.xom]

001  ; counter controlled iteration
002  
003  process
004    local counter num initial {1}
005    repeat
006      exit unless num <= 10
007      output "%d(num) "
008      do when num modulo 2 = 0
009        output "is even%n"
010      else
011        output "is odd%n"
012      done
013      increment num
014    again

Topic List


Tasks

Task 1

Write a program which outputs the five lines:

Starting now...
one
two
buckle my shoe
...Ending now.

The first and the last lines should be generated by process-start and process-end rules respectively and the middle three lines in a single process rule.

Task 2

In a new program, declare a global stream variable. In a process-start rule assign "A walk" to the variable. In a single process rule use one action to append " in" and one action to append " the wilderness" to the stream variable.

In a process-end rule, output the contents of the variable.

Task 3

Write a program which contains a global counter initialised to your current age in years and another initialised to the age in years you would like to attain. In a single process rule, declare another counter and assign to it the number of hours you expect you still have left to live. Assume there are exactly 365 days in each year and 24 hours in each day.

Output the result after a suitable short message.

Task 4

Create a global switch called 'upper', initially true, and two global streams 'word1' and 'word2' each containing a few words as their initial values.

Write a program which outputs both words with the longest word appearing first. If the switch 'upper' is true, the output should be in all uppercase letters, if it is false, the output should appear in all lower case.

Experiment with your program by modifying the initial values of the variables.

Task 5

Write a program which outputs all the times from 12 noon to 4pm in increments of one minute. The output could be something like

12:00
12:01
12:02
12:03
...
12:58
12:59
01:00
01:02
...
01:58
01:59
...


Sample Solutions

Solution 1

I have output three lines with the one output action here by embedding the '%n' (newline) code in the output stream.

[Code Sample: C02S01.xom]

001  process-start
002    output "Starting now...%n"
003  
004  process-end
005    output "...Ending now.%n"
006  
007  process
008    output "one%ntwo%nbuckle my shoe%n"

Solution 2
[Code Sample: C02S02.xom]
001  global stream river
002  
003  process-start
004    set river to "A walk"
005  
006  process-end
007    output "%g(river)%n"
008  
009  process
010    set river to river join " in"
011    set river to river join " the wilderness"

Solution 3
[Code Sample: C02S03.xom]
001  global counter currentAge initial {40}  ;; I wish!
002  global counter finalAge initial {90}    ;; who knows?
003  
004  process
005    local counter hours
006    set hours to (finalAge - currentAge) * 365 * 24
007    output "I have about %d(hours) hours left!%n"

Solution 4
[Code Sample: C02S04.xom]
001  global switch upper initial {true}
002  global stream word1 initial {"The quick brown fox"}
003  global stream word2 initial {"Sam the man owned the fox"}
004  
005  
006  process
007    local counter len1
008    local counter len2
009    set len1 to length of word1
010    set len2 to length of word2
011  
012    do when len1 > len2
013      do when upper
014        output "%ug(word1)%n%ug(word2)%n"
015      else
016        output "%lg(word1)%n%lg(word2)%n"
017      done
018    else
019      do when upper
020        output "%ug(word2)%n%ug(word1)%n"
021      else
022        output "%lg(word2)%n%lg(word1)%n"
023      done
024    done

Solution 5

The format modifier '%2fzd' can be used on a counter to output it in 2 character spaces, fixed length, zero padded.

[Code Sample: C02S05.xom]

001  process
002    local counter hours initial {12}
003    local counter minute initial {0}
004  
005    repeat
006      exit when hours = 4
007      set minute to 0
008      repeat
009        exit when minute = 60
010        output "%2fzd(hours):%2fzd(minute)%n"
011        increment minute
012      again
013      increment hours
014      set hours to 1 when hours = 13
015    again