Record types

Record types do not introduce a new kind of XDM value; rather, they provide a new way of constraining the content of a map. For example, the record type record(longitude as xs:double, latitude as xs:double) matches a map if it has exactly two entries, with the keys "longitude" and "latitude" (as instances of xs:string), and associated values of type xs:double.

Wherever an item type can be used (for example, in the type signature of a function or variable, or in an instance of expression), Saxon allows a record type to appear. The syntax is:

recordType ::= "record" "(" fieldDef ("," fieldDef)* ("," "*")? ")" fieldDef ::= fieldName "?"? ("as" fieldType)? fieldType ::= sequenceType | (".." occurrenceIndicator?)

where fieldName is either an NCName or a string literal.

A "?" after a fieldName indicates that the field is optional.

A final ", *" at the end of the list of fields indicates that the record type is extensible: additional fields beyond those that are explicitly declared are permitted to appear in the map.

If the "as" clause is absent for any field, the assumed type is item()*.

The special field type ".." allows a record type to be recursive: it is a reference to the containing record type. See Recursive record types below.

For example:

record(ssn as xs:string, emp as element(employee))

or:

record('first name', 'middle initial', 'last name', *)

The instances of a record type are maps. A record type is essentially a way of defining a more precise type for maps. The first example will be satisfied by any map with two entries: an entry whose key is "ssn" and whose value is a string, and an entry whose key is "emp" and whose value is an employee element. The second example matches any map with entries having the keys "first name" and "middle initial" and "last name", regardless of the types of the values; the map may also contain additional entries, with any key and any value.

More formally, a value M is an instance of a record type record(N1 as T1, N2 as T2, ...) if and only if all the following conditions are true:

  1. M is a map

  2. For every (N, T) pair in the record definition that is not declared optional (that is, there is no "?" after the field name), map:contains(M, N) returns true.

  3. For every (N, T) pair in the record definition, if map:contains(M, N) returns true then map:get(M, N) instance of T also returns true.

  4. If the record type is not extensible (that is, there is no final ", *" after the list of field names), then every key present in the map is a string equal to one of the field names appearing in the record definition, when compared using codepoint collation.

So for every field defined in the record definition, the map will typically contain an entry whose key matches the field name and whose value matches the corresponding type. The entry is allowed to be absent only if the type definition indicates that an empty sequence would be acceptable. Additional entries are allowed only if the type definition indicates that it is extensible.

Since a record is a map, fields can be accessed using the lookup syntax $T?NNN where $T is an expression that returns a record, and NNN is the name of the field. A separate 4.0 extension allows the field name to be written as a string in quotes if it is not an NCName, for example $T?"date of birth".

Using records makes stronger type checking possible. With a general-purpose map, any atomic value can be supplied as a key, and the map:get() function (or a dynamic call on the map as a function) returns an empty sequence if the entry is not present. With records, however, supplying a key value that is not a string, or that is not one of the known field names, results in a static type error if it can be detected at compile time. Furthermore, if $T is known to be a record type, then the type of an expression such as $T?field can be statically inferred, which again enables better error detection and better optimization.

Record types are useful where maps are used to hold information with a regular structure; for example when a function accepts maps as input or returns maps as output. Declaring the argument or result type of a function as a record type often gives better information about the expected contents than when it is declared as a map.

Record types can be used as patterns, to match maps (regardless whether the value was originally declared as a record or not). This can be especially useful when processing JSON. For example, if a JSON document containing the object {'long': 23.1234, 'lat':55.624} is parsed using the parse-json() or json-doc() functions, then the resulting map can be processed using a template rule declared with match="record(lat, long)".

Record types fit into the type hierarchy as subtypes of maps. A record type T1 is a subtype of another record type T2 if every possible instance of T1 is a permitted instance of T2 (this takes into account whether either or both record types are extensible). Similarly, a record type T is a subtype of a map type M if every possible instance of T is a permitted instance of M. By extension, because maps are functions, records are also functions, and record types are thus subtypes of function types.

To make it easier to use record types without sacrificing portability, Saxon allows you to declare the type in both portable and Saxon-specific syntax, for example <xsl:param name="location" as="map(xs:string, xs:double)" saxon:as="record(x as xs:double, y as xs:double)"/>.

A map that contains additional entries beyond those defined in an extensible record type still conforms to the type (so if the expected type of a function argument is record(a as xs:integer, b as xs:integer, *), then you can supply any map that contains these two fields, even if it also contains others). A consequence of making a record type extensible is that incorrect static references are less likely to result in a compile-time error.

The following XSLT example shows a couple of functions from a library designed to perform complex number arithmetic, where a complex number is defined as the type record(r as xs:double, i as xs:double):

<xsl:function name="cx:complex" as="record(r as xs:double, i as xs:double)"> <xsl:param name="real" as="xs:double"/> <xsl:param name="imag" as="xs:double"/> <xsl:sequence select="map{'r':$real, 'i':$imag}"/> </xsl:function> <xsl:function name="cx:add" as="record(r as xs:double, i as xs:double)"> <xsl:param name="x" as="record(r as xs:double, i as xs:double)"/> <xsl:param name="y" as="record(r as xs:double, i as xs:double)"/> <xsl:sequence select="cx:complex($x?r + $y?r, $x?i + $y?i)"/> </xsl:function>

The same functions could be written in XQuery:

declare function cx:complex ($real as xs:double, $imag as xs:double) as record(r as xs:double, i as xs:double) { map{'r':$real, 'i':$imag} }; declare function cx:add ($x as record(r as xs:double, i as xs:double), $y as record(r as xs:double, i as xs:double)) as record(r as xs:double, i as xs:double) { cx:number($x?r + $y?r, $x?i + $y?i) };

Two additional extensions help to improve the usability of record types:

Recursive record types

The syntax ".." allows a field of a record type to reference the containing record type, thus allowing recursive structures. For example a simple linked list can be declared using the type:

record(value, next as ..?)

While a binary tree can be declared as:

record(left? as .., value, right? as ..)

It will only be possible to create a finite instance of a recursive record type if the self-reference is made optional, either by making the field itself optional (as in the second example), or by allowing it to take an empty sequence as its value (as in the first example).