OmniMark provides a number of different simple data types, such as integer, switch, and string. Shelf items of these types, however, can only hold a single value. In many cases you may want a shelf item that can hold multiple related values. Examples include shelves that describe the metadata fields attached to a document or shelves that describe a point on a two-dimensional grid.

Here is how you would declare a record type to hold the metadata for a document:

  declare record metadata-type
     field string  author
     field string  title
     field string  publisher
     field integer 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 shelf of that type would be declared, substituting the word field for the word local.

However, the record declaration by itself only defines a type. It does not create an actual shelf. 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 shelf of type metadata-type, you use a standard local or global declaration:

     local metadata-type document-information

Once you have a shelf of type metadata-type, you can read and write the individual fields of the record instance. To address a particular field of a record instance, you use the shelf name, followed by a colon, followed by the field name:

     local metadata-type document-information
     set document-information:author    to "Sir Arthur Conan Doyle"
     set document-information:title     to "The Hound of the Baskervilles"
     set document-information:publisher to "George Newnes"
     set document-information:year      to 1902
     set author-name to document-information:author
     output document-information:title

You can pass a record instance to a function:

  define string source function 
     format-metadata (value metadata-type x)
     output "<H1>%n"
         || x:title
         || "</H1><P><B>by "
         || x:author
         || "</B></P>%n<P>"
         || x:publisher
         || " (" || "d" % x:year || ") "
     output "<I>out of print</I>" 
        unless x:in-print
     output "</P>"

You can also return a record instance from a function:

  define metadata-type function 
     get-document-information ()
     local metadata-type x
     set x:author    to "Sir Arthur Conan Doyle"
     set x:title     to "The Hound of the Baskervilles"
     set x:publisher to "George Newnes"
     set x:year      to 1902
     return x

Initializing a record

You can initialize a record instance by writing a function that takes the field values as arguments and returns a record instance:

  define point-type function 
     make-point (value integer x,
                 value integer y) 
     local point-type a
     set a:x to x
     set a:y to y
     return a

You can then call this function to initialize a record instance, either when the shelf is declared, or in the body of a rule or function:

     local point-type foo
     local point-type bar initial { make-point (12, 32) }
     set foo to make-point (14, 81)

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 instance to a string. The following conversion function converts a point-type record instance to a comma-separated pair of values:

  define string conversion-function value point-type p
     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:

     local point-type foo initial { make-point (23, 86) }
     output foo

Operations on records

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 two instances of the type point-type together. You could write a function specifically to add point-types:

  define point-type function 
     add-points (value point-type a,
                 value point-type b) 
     local point-type 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 instances of point-types. This can be done by overloading OmniMark's + operator:

  define overloaded point-type infix-function
        value point-type a
        value point-type b
     local point-type 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 point-type instances together like this:

     local point-type foo initial { make-point (23, 86) }
     local point-type bar initial { make-point (74, 98) }
     local point-type baz
     set baz to foo + bar


While you can treat record shelves like regular shelves for most purposes, there are some important differences. Record shelf items are not in fact records themselves, the way an integer shelf item really is an integer. A record shelf item is actually a reference to a record instance.

As long as you address the individual fields of a record, this makes no difference. The difference comes when you use a record shelf item alone. Consider what happens in the following code:

     local integer i initial { 2 }
     local integer j initial { 4 }
     set j to i
     set i to 5
     output "d" % j

As expected, this program outputs 2, the last value assigned to the shelf j.

Now consider this program:

  declare record point-type
     field integer x
     field integer y
     local point-type alpha
     local point-type 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 assigned alpha to omega. However, if you run the program you will see that the actual output is 12|15, the last values assigned to alpha.

This happens because alpha and omega are both references to record instances, not the names of the records themselves. When we assigned alpha to omega therefore, we made the shelf omega reference the same record instance as the shelf alpha. That left us with two references to one record instance and no references to the other record instance. When we changed the values of the record instance referenced by alpha, we were also changing the values of the record instance referenced by omega, since both alpha and omega reference the same instance.

Since there were no references to the orphaned record instance that used to be referenced by omega, that record instance 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.

Copying records

But if set copies references to records, how do you copy the values of one record instance 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 instance of point-types:

  define function 
     copy-point (value point-type a,
                 value point-type b)
     set b:x to a:x
     set b:y to a:y
     ; ...
     copy-point (alpha, omega)

You may notice something odd about this function: Both a and b are passed to copy-point 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 record instances, not records themselves. It is the references that are passed by value and the references are not being changed, only the record instances that they refer to.