function, define function

declaration/definition

Syntax
define result-class? result-type? function 
   function-name argument-list? 
((as function-body) | elsewhere)
    


Purpose

You can define a function using define function.

This is a simple example of a function definition:

  define integer function 
     add (value integer x, 
          value integer y)
  as
     return x + y
          

A function must be defined before it can be called. That is, the definition of the function must occur in the source code before any calls to the function.

Function name

The function name follows the function keyword and the data type, if specified. The function name must be a legal OmniMark name. The name of the function defined above is add.

Value returning functions

A function may return a value. If so, the data type of the return value is specified between the define and function keywords. The type may be any of the OmniMark data types or any user-defined record type, or any opaque data type.

You can call a function anywhere you can use an expression of the same type. For example, here the integer function add (defined above) is used as part of a numeric (integer) expression:

  process
     local integer i
  
     set i to 27 - add (3,4)

Shelf-class functions

A function may return a reference to a shelf. If so, the data type of the shelf reference must be specified, but unlike the simple expression-class functions discussed previously, a shelf class must also be specified. The allowed classes are read-only, modifiable, and write-only. The allowed data types are the basic data types, the stream data type, the markup data types, an opaque type, or a record type.

You can call a shelf-class function anywhere you can use a shelf reference of the same type. For example, here a read-only integer shelf-class function some-primes is used in a repeat over loop:

  define read-only integer function
     some-primes ()
  as
     return { 2, 3, 5, 7, 11, 13, 17 }
  
  
  process
     repeat over some-primes () as p
        output "d" % p || "%n"
     again

string source functions

A function may return a stream of input to the calling environment. An string source function is like a stream returning function in that it returns a stream to the calling environment. However, an string source function differs from a stream returning function in that its return value is composed of all the output generated while the function is executing (unless that output is specifically directed to another destination). The following string source function scans a stream, escapes the markup characters, and returns the escaped stream:

  define string source function 
     escape-text value string original
  as 
     repeat scan original
     match [\"&<>"]+ => ordinary
        output ordinary
  
     match "&"
        output "&amp;"
  
     match "<"
        output "&lt;"
  
     match ">"
        output "&gt;"
     again

string source functions run as co-routines.

Action functions

You can also define a function that does not return a value. Such a function is commonly called an "action function" because it is used just like an OmniMark action. To define an action function, simply omit the return type:

  define function 
     output-sum (value integer x, 
                 value integer y)
  as
     output "d" % x + y

You call an action function just as you would use a regular OmniMark action:

  process
     output-sum (12,87)

While an action function does not return a value, it may modify the value of a shelf that is passed to it. The following function sets all the values of a switch shelf to false.

  define function 
     flip (modifiable switch flags)
  as 
     repeat over flags
        set flags to !flags
     again
     
  process
     local switch status initial { true, true, false, true }
  
     flip (status) 
  
     repeat over status
        do when status = true
           output "TRUE%n"
  
        else
            output "FALSE%n" 
        done
     again

Function arguments

You can use arguments to pass information to a function.

The declaration of a function argument has four parts:

  1. The method of passing the argument to the function. This can be modifiable, read-only, write-only, value, or remainder. The meaning of these keywords is discussed below.
  2. The data type of the argument. This can be any of the OmniMark data types, any record type, or any opaque data type. It can also be of source or output type, though the output type may only be used with external functions.
  3. The name of the argument. This becomes a local variable name within the function. It must be a legal OmniMark name.
  4. The optional optional keyword. If an argument is declared optional, it need not be included in the call to the function.

The maximum number of arguments for a function call is 16383.

How arguments are passed to a function

All OmniMark variables are shelves. OmniMark gives you four ways to pass a shelf as an argument to a function:

  1. Pass a reference to the shelf and allow the function to read and to alter the shelf. (modifiable)
  2. Pass a reference to the shelf and allow the function to alter the shelf (but not read the shelf). (write-only)
  3. Pass a reference to the shelf and allow the function to read the shelf (but not to alter the shelf). (read-only)
  4. Make a copy of a single item on the shelf, pass it to the function, but do not let the function alter the copy. (value, remainder)

Modifiable arguments

If an argument is declared modifiable, the following conditions apply:

  • The function call must pass an actual existing shelf, not a constant or the result of a calculation.
  • The function has full control of the shelf.
  • The type of the shelf must match the declared type.
  • If the function call specifies a particular item on the shelf, that item is treated as the default item in the function, but the whole shelf is still available.

The following example uses a modifiable argument:

  define function 
     decrement-all (modifiable integer the-numbers) 
  as
     repeat over the-numbers
        decrement the-numbers
     again
     
  
  process
     local integer my-numbers size 5 initial { 3, 5, 7, 9, 11 } 
  
     decrement-all (my-numbers)
     repeat over my-numbers
        output "%d(my-numbers)%n"
     again

Write-only

write-only arguments are chiefly useful when dealing with extended records.

If an argument is declared write-only, the following conditions apply:

  • The function call must pass an actual existing shelf, not a constant or the result of a calculation.
  • The function can write to the shelf.
  • The function cannot read from the shelf.
  • The function cannot address individual fields of an item on the shelf, it can only add or remove whole items.
  • The type of the shelf must match the declared type or be a type from which the declared type is an extension.
  • If the function call specifies a particular item on the shelf, that item is treated as the default item inside the function, but the whole shelf is still available.

The following example uses a write-only argument. The function add-novel has a write-only argument of type "novel". However, the shelf passed to this function by the process rule is of type "publication". This is allowed for a write-only argument because "novel" is an extension of "book" which is an extension of "publication". This means that a record of type "novel" can be written to a shelf of type "publication". The write-only argument type allows such a shelf to be passed to a function. Note also that the function cannot address the fields of objects on the shelf this-novel because it does not know what types they are. (They might be books, or publications instead of novels.) To add an item to the shelf, therefore, the function declares a local record n of type "novel", sets the values of its fields, and then assigns n to the this-novel shelf.

  declare record publication
     field string publication-name
     field string publisher
  
  declare record book extends publication
     field string author variable
     field string year-of-publication
     field string binding
     field string ISBN
   
  declare record novel extends book
     field string genre
     field string locale variable
   
  define function 
     add-novel (write-only novel this-novel)
  as
     local novel n
  
     set n:publication-name to "The Wailing Wind"
     set n:publisher to "HarperCollins"
     set new n:author to "Tony Hillerman"
     set n:year-of-publication to "2002"
     set n:binding to "paperback"
     set n:ISBN to "0061098795"
     set n:genre to "mystery"
     set new n:locale to "Four Corners"
     set new this-novel to n
    
  
  process
     local publication reading-material variable
  
     add-novel (reading-material)

Read-only arguments

If an argument is declared read-only, the following conditions apply:

  • The function call must pass an actual existing shelf, not a constant or the result of a calculation.
  • The type of the shelf must match the declared type or be an extended type of the declared type.
  • The function cannot modify the shelf.
  • The function can read any item on the shelf, provided that item is readable (an open stream cannot be read).
  • The function can read keys and status information about the shelf and its items.
  • If the function call specifies a particular item on the shelf, that item is treated as the default item in the function, but the whole shelf is still available.

The following sample uses a read-only argument:

  global stream animals size 3 initial { "dog", "cat", "cow" } 
  
  define function 
     output-all (read-only stream things-to-output) 
  as
     repeat over things-to-output
        output things-to-output 
           when things-to-output is closed
     again
     
  
  process
     output-all (animals)

Value arguments

If an argument is declared value, the following conditions apply:

  • The function call may pass an item from an existing shelf or a constant or the result of a calculation.
  • The type of the value must match the declared type or be an extended type of the declared type.
  • The function call cannot pass a shelf item that is not readable (an open stream cannot be read).
  • The item is passed as a new one-item shelf. The key of the original item is not passed to the new shelf.
  • The function can read the new shelf but cannot modify it.

The following sample uses value arguments:

  global integer a initial { 7 }
  
  define integer function 
     add (value integer x, 
          value integer y)
  as
     return x + y
     
  
  process
     output "d" % add (2 + 6, add (a * 12, 9))

Remainder arguments

You can also pass a number of value arguments of the same type to a function using a "remainder" argument.

If an argument is declared remainder, the following conditions apply:

  • The function call can pass an unlimited number of value arguments.
  • The type of the value must match the declared type or be an extended type of the declared type.
  • The function receives a shelf of value arguments.
  • The remainder argument must be the last argument of the function.
  • There can be only one remainder argument.

The following sample uses a remainder argument:

  define function 
     prefixed-output (value     string prefix,
                      remainder string items)
  as
     repeat over items
        output prefix || items
     again
     
  
  process
     prefixed-output ("<item>" , "red%n", "yellow%n", "blue%n")

When you use only a single remainder argument, you must include an ellipsis (three periods) in your function definition as shown below:

  define integer function 
     sum (remainder integer x, ...)
  as
     local integer a initial { 0 }
  
     repeat over x
        set a to a + x
     again
  
     return a
     
  
  process
     output "d" % sum (2, 3, 4, 6)

Optional arguments

You can declare an argument optional, which simply means that you do not need to specify a value for that argument when you call the function. To declare an argument optional, place the optional keyword after the argument name.

If an argument is declared optional, the function must deal with the possibility that the argument value has not been specified. You can do this one of two ways.

  • You can specify a default for the argument.
  • You can test to see if the argument is specified using is specified.

The following code illustrates the use of initial to specify a default on a value argument:

  define string function 
     number-to-string (value integer the-number,
                       value integer the-base optional initial { 10 }) 
  as
      local string format-string
  
      set format-string to ("d" % the-base) || "rd"
  
      return format-string % the-number
      
  
  process
     local integer my-number initial { 23456 }
     
     output "Decimal: " || number-to-string (my-number) 
         || "%nHexadecimal: " || number-to-string (my-number, 16) 
         || "%nOctal: " || number-to-string (my-number, 8) 
         || "%nBinary: " || number-to-string (my-number, 2) 

The following code illustrates the use of is specified to test whether an optional parameter has been passed:

  define string function 
     number-to-string (value integer the-number,
                       value integer the-base optional) 
  as
      local string format-string
  
      do when the-base is specified
          set format-string to ("d" % the-base) || "rd"
  
      else
          set format-string to "d"
      done
  
      return format-string % the-number
  
      
  process
     local integer i initial { 23456 }
     
     output "Decimal: " || number-to-string (i) 
         || "%nHexadecimal: " || number-to-string (i, 16) 
         || "%nOctal: " || number-to-string (i, 8) 
         || "%nBinary: " || number-to-string (i, 2) 

The initial value of optional function arguments can be dynamic values. For example, the following function outputs a "modified" tag and dynamically calculates the default value of the "when" argument:

  define string source function 
     modified-tag (value string by,
                   value string why,
                   value string when optional initial { date "=xY-=xM-=xD =xH:=xm:=xs" })
  as
     output "<modified by='" || by
         || "' why='" || why
         || "' when='" || when
         || "'>%c</modified>"

With the conventional parentheses-and-comma style of function arguments, you are only permitted one optional argument, it must be the last argument, and cannot be the only argument. The heralded form, discussed below, gives you greater flexibility.

Source parameters

A source parameter is used to pass an OmniMark source to a function. OmniMark has several built-in sources such as #main-input. The OmniMark keyword file returns a source, as do many external functions. In the following example, an string source function is written to convert any specified source to lowercase. In the example, the source argument is provided by a call to the file function:

  define string source function 
     lower-case value string source s
  as
     repeat over s
     match any => c
        output "lg" % c
     again
  
  
  process
     output lower-case file "out.txt"

Function body

You introduce the body of your function with the keyword as. The body of the function consists of a sequence of OmniMark statements, just as in the body of a rule.

Functions defined elsewhere

A function must be defined before it can be called. This is a problem if function A calls function B and function B calls function A. If A is defined before B, then A cannot legally call B. To get around this, you can pre-define B before you define A. To predefine a function, simply replace as with elsewhere and omit the function body. (Any initial values for optional arguments should also be omitted.) Of course, you must still define B fully in its place, and the full definition must match all the elements of the predefinition.

Heralded function arguments

So far, all the examples we have looked at use the conventional parentheses and commas for delimiting function arguments. The parentheses and commas are used in both the function declaration and the function call to separate one argument from another.

If you use the parentheses and commas form, it may not always be clear, when you read the resulting code, what role each of the function arguments plays.

You can make your code clearer if you use heralds instead of parentheses and commas to separate the arguments of your function. A herald is simply a word that you specify as a separator. Notice that the name of a function itself often acts as a herald, so the simplest form of a heralded function is one with a single argument and no parentheses:

  define function 
     output-twice value string s 
  as
     output s ||* 2
  
  
  process
     output-twice "Tom"

If you have more than one argument, you can specify heralds as argument separators. Choose heralds that identify the role that the argument is to play. A well-chosen herald will make it easier to remember how to use your function, and easier to see what the function does in your code. A herald can be any token that follows the rules for OmniMark names; however, optional cannot be used as a herald, as this would lead to an ambiguity.

  define function 
     output-repeatedly         value string  s
                       repeats value integer repetitions
  as
     output s ||* repetitions
  
  
  process
      output-repeatedly "Hip hip hooray!%n" repeats 3

While the function name itself is sometimes an adequate herald for the first argument, you will often want to specify a herald for the first argument (it is common and acceptable for the heralds and the argument names to be the same):

  define integer function 
     calculate-volume height value integer height
                      width  value integer width
                      depth  value integer depth
  as
     return height * width * depth

This is what the function call might look like:

  set volume to calculate-volume height 12 width 7 depth 4 

An important property of heralded function arguments is that they allow you to have more than one optional argument:

  define string function 
     number-to-string            value integer the-number
                      using-base value integer the-base  optional initial { 10 }
                           width value integer the-width optional 
  as
      local string format-string
  
      do when the-width is specified
          set format-string to ("d" % the-base) 
                            || "r"
                            || ("d" % the-width)
                            || "fzd"
  
      else
          set format-string to ("d" % the-base) || "rd"
      done
  
      return format-string % the-number
      
  
  process
     local integer i initial { 23456 }
     
     output "Decimal: " || number-to-string i 
         || "%nHexadecimal: " || number-to-string i using-base 16 width 8 
         || "%nOctal: " || number-to-string i using-base 8 
         || "%nWide: " || number-to-string i width 12 

Note that while it is legal to use the same herald for more than one argument of your function, you cannot have two consecutive arguments with the same herald if the first argument is optional, because it would be ambiguous as to which argument was intended by using the herald.

You can also use remainder arguments with heralded function names. In the case of a remainder argument with a herald, you must repeat the herald before each item in the function call.

  define string source function 
     concatenate     value     string a
                 and remainder string b
  as
     output a
     repeat over b
        output b
     again
      
  
  process
     output concatenate "This " and "dog " and "has " and "fleas."

Calling heralded functions

When you call a function that uses conventional parentheses and commas to separate function arguments, you can pass a complex expression as a function argument. In the code below, the expression "6 * 5" is used as a function argument:

  define integer function 
     add (value integer x,
          value integer y)
  as 
     return x + y
      
  
  process
     output "d" % add (4, 6 * 5)

If you use heralded function arguments, however, you must place complex expressions in parentheses:

  define integer function 
     add    value integer x
         to value integer y
  as 
     return x + y
      
  
  process
     output "d" % add 4 to (6 * 5)

If you omit the parentheses, OmniMark will recognize the first simple expression ("6" in this case) as the whole argument, and will raise a compile-time error when it sees the rest of the expression (unless, by chance, the rest of the expression constitutes valid OmniMark code in that place). This rule avoids the possibility of ambiguity in recognizing argument separators.