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.
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'.
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:
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.
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.
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
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.
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.
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.
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?
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
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.
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.
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"
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
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.
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.
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.
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.
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 ...
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"
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"
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"
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
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