Functions: overloaded

If you need to write a function that performs the same operation on data of different types, you will need to write overloaded functions. For this example we will write a function that implements a guzinta operation. A guzinta ("goes into") is a division standing on its head, so that if 6 / 2 = 3, 2 guzinta 6 = 3.

Here is a simple guzinta function for integers. Because we want the function name, guzinta, to go between its two arguments, rather than before them, we define it as an infix-function.

  define integer infix-function
   value integer a
   guzinta
   value integer b
   as
     return b/a
  
  process
     local integer foo
     set foo to 3 guzinta 12
     output "d" % foo

This implements the guzinta operator for integers. But we may also want to perform a guzinta calculation for floating point numbers. Here is the guzinta function for floats:

  import "omfloat.xmd" unprefixed
  
  define float infix-function
   value float a
   guzinta
   value float b
   as
     return b/a
  
  process
     local float foo
     set foo to 3.5 guzinta 15.75
     output "d" % foo

This works fine. But what if you want to perform guzinta calculations on integers and floats in the same program, just as you can perform multiplication, division, addition and subtraction on both types using the same operators? To do this you need two different guzinta functions in the same program. To make this possible, there must be a way for OmniMark to tell which function is being called in each case. This is accomplished by defining a pair of overloaded functions:

  import "omfloat.xmd" unprefixed
  
  define overloaded integer infix-function
   value integer a
   guzinta
   value integer b
   as
     return b/a
  
  define overloaded float infix-function
   value float a
   guzinta
   value float b
   as
     return b/a
  
  process
     local integer ifoo
     local integer ix initial {3}
     local integer iy initial {15}
  
     local float ffoo
     local float fx initial {3.5}
     local float fy initial {15.75}
  
     set ifoo to ix guzinta iy
     output "d" % ifoo
  
     output "%n"
  
     set ffoo to fx guzinta fy
     output "d" % ffoo

This program works for both integer and float arguments. OmniMark knows which function to call by inspecting the types of the arguments passed to the function. If the arguments are both of type float, then the float version is called. If the arguments are both of type integer, then the integer version is called.

What happens if the arguments are of differnet types? What if we add the following lines to our program?

     set ffoo to fx guzinta iy
     output "d" % ffoo

This code will produce a runtime error with the message:

  Operator type mismatch
  An argument for operator 'GUZINTA' is of the wrong type. The type
  signature is 'FLOAT GUZINTA INTEGER'.

This is different from the behavior we would have seen from the original un-overloaded float version of the gunzinta function. With that version, passing an integer value as one of the arguments would have triggered an automatic conversion of the integer value to a float value, and the function would have executed normally and returned a float value.

However, if OmniMark is to use the type of the arguments of an overloaded function to decide which version of the function to call, then it cannot apply an automatic conversion of one type to another. Therefore, when you define overloaded functions, you have to take care of every possible combination of types that could be passed to the functions.

If we want to be able to perform guzinta operations on a mix of float and integer values, therefore, we need to provide overloaded functions for "float guzinta integer" and "integer guzinta float". Before we do this, however, we need to decide which type the result the guzinta operator should produce for each of these cases. When you perform an operation on data of two different types you risk losing information if you create a result of the wrong type. You therefore need to establish a hierarchy of types. In this case the choice is simple. Floats contain fractional information and integers don't, so in the case of an operation on integers and floats, we want the answer to be expressed as a float.

We can define two more functions to cover the "float guzinta integer" and "integer guzinta float" cases:

  define overloaded float infix-function
   value integer a
   guzinta
   value float b
   as
     return b/a
  
  define overloaded float infix-function
   value float a
   guzinta
   value integer b
   as
     return b/a

However, if you have more than two data types you need to handle, it will get cumbersome to provide all the possible combinations of types. To avoid this problem we can consolidate some of the variants. The following sample consolidates "float guzinta float" and "float guzinta integer":

  define overloaded float infix-function
   value (float | integer into float) a
   guzinta
   value float b
   as
     return b/a

Here we join the different types with the | keyword and specify the type into which the arguments must be converted using the into keyword. The parentheses are essential, and the into part must be specified when using this form.

This leaves us with three overloaded guzinta functions:

  define overloaded integer infix-function
   value integer a
   guzinta
   value integer b
   as
     return b/a
  
  define overloaded float infix-function
   value (float | integer into float) a
   guzinta
   value float b
   as
     return b/a
  
  define overloaded float infix-function
   value float a
   guzinta
   value integer b
   as
     return b/a

It is tempting to try and consolidate further by writing a function that looks like this:

  define overloaded float infix-function
   value (float | integer into float) a
   guzinta
   value float (float | integer into float) b
   as
     return b/a

However, this won't work because this definition includes the combination "integer guzinta integer", which has already been defined. You must make sure that each possible combination of types is defined only once.

There are a couple of types that we still have to include in our function definitions, however. Consider what happens if we try the following calculation with our current set of guzinta functions:

  process
     local float foo
     set foo to 3.5 guzinta "15.75"
     output "d" % foo

This program will produce the error:

  An argument for operator 'GUZINTA' is of the wrong type. The type
  signature is 'NUMERIC-LITERAL GUZINTA STRING'.

We are used to stream values and numeric literals (numbers expressed directly in program code) being accepted by OmniMark's mathematical operators. However, these values are both in the form of strings, and in an overloaded function, automatic conversion of these strings to integers or floats is not possible. We have to make the type selection and the conversion explicit by adding overloaded functions to handle them. In doing so, we have to decide what type we will return when a calculation includes an numeric literal or a stream value. Since we cannot tell in advance whether a string or numeric literal contains an integer or float value, we will make all guzinta calculation involving strings or numeric literals return floats.

Here are the functions to add to our set of guzinta functions to support stream and numeric literal types:

  ;-------------------------------------------
  ; numeric literal
  
  define overloaded float infix-function
   value (numeric-literal into float) a
   guzinta
   value (integer | float | stream | numeric-literal into float) b
   as
     return b/a
  
  define overloaded float infix-function
   value (integer | float into float) a
   guzinta
   value  (numeric-literal into float) b
   as
     return b/a
  
  ;-------------------------------------------
  ; stream
  
  define overloaded float infix-function
   value string a
   guzinta
   value (integer | float | stream | numeric-literal into float) b
   as
     return b/a
  
  define overloaded float infix-function
   value (integer | float into float) a
   guzinta
   value  stream b
   as
     return b/a

Overloading existing operators

The example above uses overloaded functions to introduce a new operator. A more likely use of overloading is to allow you to apply exising operators to new data types. You can create new data types by defining a record type. In the example that follows, we will define a type "point" to represent a location in a two dimensional space. We will then implement addition and subtraction operations for the "point" type by overloading the built-in + and - operators:

  declare record point
   field integer x
   field integer y
  
  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
  
  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
  
  process
     local point foo
     local point bar
     local point baz
  
     set foo:x to 7
     set foo:y to 9
  
     set bar:x to 2
     set bar:y to 3
  
     set baz to foo + bar
  
     output "baz:x = " || "d" % baz:x
         || "%n"
         || "baz:y = " || "d" % baz:y

Overloaded functions and extended types

The example above deals with basic record types. You may also want to create overloaded functions to deal with different members of a set of extended record types. An important feature of extended record types is that if a record is an extension of another record type it can be used as an item of that record type. For example, a record type point:

  declare record point
   field integer x
   field integer y
can be extended to create a record type pixel by adding a color field:
  declare record pixel
   extends point
   field stream color

Now suppose that we want to create a display function for all things pointy (that is, all types that are extensions of point). We cannot simply create a pair of overloaded display functions:

  define overloaded string source function display
   value point p
   as
     using p:x as x
     using p:y as y
     output "%d(x),%d(y)%n"
  
  define string source function display
   value pixel p
   as
     using p:x as x
     using p:y as y
     using p:color as c
     output "%d(x),%d(y):%g(c)%n"

This will cause OmniMark to complain that a display function has already been defined for type pixel because a pixel can be used as a point and there is already a display function for a point. The OmniMark compiler therefore cannot tell which function should be called when the function name is invoked with an argument of type pixel.

To avoid this problem you need a dynamically overloaded function, that is, one in which the particular function to execute is chosen while the program is running based on the particular type of the argument that is passed to it.

To do this, you write a dynamic function for the base type:

  define dynamic string source function display
   value point p
   as
     using p:x as x
     using p:y as y
     output "%d(x),%d(y)%n"

This function is the starting point for selecting a function to invoke for any argument type that is an extension of point. It is the dynamic function for point and its extensions. The specific functions for the individual extensions of point are not themselves dynamic functions, but overriding functions. That is, the dynamic function for point will be selected for arguments of type point and all extensions of type point unless there is an overriding function defined for the specific type of the argument. Here is an overriding function for type pixel.

  define overriding string source function display
   value pixel p
   as
     using p:x as x
     using p:y as y
     using p:color as c
     output "%d(x),%d(y):%g(c)%n"

Here is how these dynamic functions might get called. Notice that the decision about which function to call must be made at run time since the same function call in the repeat loop has a different argument type each time:

  process
     local point pointy-thing variable
     local point a
     local pixel b
  
     set a:x to 5
     set a:y to 89
  
     set b:x to 76
     set b:y to 231
     set b:color to "cyan"
  
     set new pointy-thing to a
     set new pointy-thing to b
  
     repeat over pointy-thing
        output display pointy-thing
     again

It is very possible that you will want to define display functions for many different record types. You can do this by defining functions that are both statically and dynamically overloaded. Here is a dynamic display function definition for the root type publication and one of its extensions, novel:

  define dynamic string source function display
   value publication p
   as
     output p:name || "%n"
  
  define overriding string source function display
   value novel n
   as
     output n:name
         || " a "
         || n:genre
         || " novel%n"

If you want to have a display function both publications and pixels, you simply add the word overloaded to the dynamic functions in each case:

  define overloaded dynamic string source function display
   value publication p
   as
     output p:name || "%n"
  
  define overriding string source function display
   value novel n
   as
     output n:name
         || " a "
         || n:genre
         || " novel%n"
  
  define overloaded dynamic string source function display
   value point p
   as
     using p:x as x
     using p:y as y
     output "%d(x),%d(y)%n"
  
  define overriding string source function display
   value pixel p
   as
     using p:x as x
     using p:y as y
     using p:color as c
     output "%d(x),%d(y):%g(c)%n"

Notice that only the dynamic functions are declared overloaded, not the overriding functions. The appropriate dynamic function to call can be determined by OmniMark at compile time, which is what the overloaded keyword indicates. The appropriate overriding function for that type is then determined at runtime.

Dynamic and conversion functions working together

It is often useful to create display functions for the record types that you create. The display functions discussed above are all defined as string source functions, which makes it easy to call them with the output keyword or to feed them to any other context that accepts a stream of text:

     repeat over pointy-thing
        output display pointy-thing
     again

However, it would be even neater to simply output (or scan, or generally use an object as a source) without specifically invoking the display function:

     repeat over pointy-thing
        output pointy-thing
     again

You can do this by writing a conversion function that calls a dynamic function:

  define input conversion-function
   value point p
   as
     output display p

This technique is useful because it is not possible to write a conversion function for every member of a set of extended types. However, by writing a dynamic set of display functions for a type and its extensions, you can use a single conversion function for the entire type hierarchy, as shown above.

When to overload

When you write overloaded functions, you should bear in mind that overloading should express the notion that the same action is being performed on data of different types. That is, the same symbol should have the same general semantics throughout your program. If you use overloading simply to reuse a symbol, but give that symbol a significantly different meaning, you may create considerable confusion for the person who must maintain your program. However, it is also important to remember that the type of the data being operated on creates a context in which the meaning of the operation is understood, and that as long as your overloaded name or symbol will be interpreted consistently in the context in which it occurs, you should be safe to overload. If you are not sure if your overloaded symbol or keyword will be interpreted correctly, it is better to choose a distinct name.