|
|||||
|
|||||
Related Syntax | |||||
Records |
You can use records to create user defined data types. OmniMark provides a number of different simple data types, such as integer, switch, and stream. A simple variable, however, can only hold a single value. In many cases you may want a variable that can hold multiple related values. Examples include variables that describe the metadata fields attached to a document or variables that describe a point on a two-dimensional grid.
Here is how you would declare a record type to hold a simple metadata record for a document:
declare record metadata-label field stream author field stream title field stream publisher field stream year field switch in-print initial {true}
Each field in the record is itself of a particular type and is declared just as a local variable of that type would be declared, substituting the word field
for the word local
.
However, the record declaration by itself only defines a record type. It does not create an actual record variable. Even though you can declare initial values for the fields, this only specifies what the initial values of those fields will be when an actual record is created.
To create an actual record of type metadata-label, you use a standard local or global declaration:
local metadata-label doc-info
Once you have a specific variable of type metadata-label, you can read and write the individual fields of the record. To address a particular field of a record, you use the variable name, followed by a colon, followed by the field name:
set doc-info:author to "Mark Baker" set doc-info:title to "Internet Programming with OmniMark" set doc-info:publisher to "Kluwer Academic Publishers" set doc-info:year to "2000" set author-name to doc-info:author output doc-info:title
You can pass a record to a function:
define input function format-metadata value metadata-label x as output "<H1>%n" || x:title || "</H1><P><B>by " || x:author || "</B></P>%n<P>" || x:publisher || "(" || x:year || ") " output "<I>out of print</I>" unless x:in-print output "</P>"
You can also return a record from a function:
define metadata-label function get-doc-info as local metadata-label x set x:author to "Mark Baker" set x:title to "Internet Programming with OmniMark" set x:publisher to "Kluwer Academic Publishers" set x:year to "2000" return x
You can initialize a record by writing a function that takes the record values as arguments and returns a record:
define point function make-point ( value integer x, value integer y ) as local point a set a:x to x set a:y to y return a
You can then call this function to initialize a record, either when the record variable is declared, or in the body of a rule or function:
process local point foo local point bar initial {make-point (12,32)} set foo to make-point (14,81)
However, there is another way to initialize a record that may be neater and easier to use in many circumstances. This is to write a conversion function that converts a stream into a record of the appropriate type. Here is a conversion function that converts a specific string format to a point record. Note that the it uses assert
to verify the string format before creating the point.
define point conversion-function value stream coordinates as local point a assert coordinates matches (digit+ "," digit+ =|) message "String " || coordinates || " is not a valid set of coordinates." set a:x to coordinates take digit+ set a:y to coordinates drop (digit+ ",") return a
Once this function is part of your program you can initialize a point like this:
process local point foo initial {"23,86"} local point bar set bar to "123,678"
Using this technique you can initialize objects directly from input data by placing the parsing code in a conversion-function.
Outputting records
You will frequently want to convert the value of a record to text for output. If you usually want the output to take the same format, you can write a conversion function to convert from a record type to a stream. The following conversion function converts a point record to a comma-separated pair of values:
define stream conversion-function value point p as return "d" % p:x || "," || "d" % p:y
Once this function is part of your program you can output a string representation of a point like this:
process local point foo initial {"23,86"} output foo
A record may be used to represent mathematical concepts, such as a point, and therefore it may be desirable to perform mathematical operations on records. For example, you may want to add points together. You could write a function specifically to add points:
define point function add-points ( value point a, value point b ) as local point c set c:x to a:x + b:x set c:y to a:y + b:y return c
However, it would be more elegant if you could use the OmniMark +
operator to add points. You can do this by writing your point adding function as an overloading of the +
operator.
define overloaded point infix-function value point a + value point b as local point c set c:x to a:x + b:x set c:y to a:y + b:y return c
Once this function is defined, you can add two points together like this:
process local point foo initial {"23,86"} local point bar initial {"74,98"} local point baz set baz to foo + bar
Once you have defined overloaded operators for your records, and have defined conversion functions to translate from strings to your record type, you may also want to extend your overloaded operators to accept the string representation of your record type:
define overloaded point infix-function value (point | stream into point) a + value point b as local point c set c:x to a:x + b:x set c:y to a:y + b:y return c define overloaded point infix-function value point a + value (stream into point) b as local point c set c:x to a:x + b:x set c:y to a:y + b:y return c
With these versions of the overloaded +
operator in place, as well as the conversion functions between points and streams, you can write code like this:
process local point foo initial {"23,86"} local point bar set bar to "123,678" output foo + bar || "%n" || foo + "176,34" || "%n" || "7,9" + bar || "%n" || point "7,9" + "176,34"
Notice that in the last case, where we add two string representations of points, it is necessary to cast one of the arguments to a point, otherwise the operation is interpreted as the adding of two strings, which is not a supported operation.
While you can treat record variables like regular variables for most purposes, there are some important differences. Record variables are not in fact records themselves, the way an integer variable really is an integer. A record variable is actually a reference to a record.
As long as you address the individual fields of a record, this makes no difference. The difference comes when you use a record variable alone. Consider what happens in the following code:
process local integer i initial {2} local integer j initial {4} set j to i set i to 5 output "%d(j)"
This program, as you would expect, outputs the number "2", the last value assigned to the variable j
.
Now consider this program:
declare record point field integer x field integer y process local point alpha local point omega set alpha:x to 2 set alpha:y to 4 set omega:x to 3 set omega:y to 9 set omega to alpha set alpha:x to 12 set alpha:y to 15 output "d" % omega:x || "|" || "d" % omega:y
You might expect the output of this program to be "2|4" since that is the set of values in the record alpha when we set omega to alpha
. However, if you run the program you will see that the actual output is "12|15", the last values assigned to alpha
.
Why does this happen? It happens because alpha
and omega
are both references to records, not the names of the records themselves. When we set set omega to alpha
therefore, we made the variable omega
reference the same record as the variable alpha
. That left us with two references to one record and no references to the other record. When we changed the values of the record referenced by alpha
, we were also changing the values of the record referenced by omega
, since both alpha
and omega
reference the same record.
What happened to the orphaned record that used to be referenced by omega
? Once there were no references to it, that record was no longer available to the program. Sooner or later, it will be cleaned up and disposed of automatically by OmniMark's garbage collector.
The fact that records are references has important consequences for how you compare two records.
But if set
copies references to records, how do you copy the values of one record to another? One way is to do it field by field:
set alpha:x to 2 set alpha:y to 4 set omega:x to 3 set omega:y to 9 set omega:x to alpha:x set omega:y to alpha:y
However, this is cumbersome. Another approach is to write a function to copy points:
define function copy-point value point a to value point b as set b:x to a:x set b:y to a:y process ... copy-point alpha to omega
You may notice something odd about this function. Both a
and b
are passed to the function as value arguments and value arguments cannot be modified. Yet we are changing the values of record b
. Why does this work? It works because a
and b
are both references to records, not records themselves. It is the references that are passed by value and the references are not being changed, only the records that they reference.
Related Syntax : (field selection operator) collect-garbage declare record export, opaque |