Function overloading allows different function implementations to use the same function name; which function is selected in a given location of your program is determined by the type of the arguments. Using function overloading can make your programs more succinct.
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 integer
s. 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 i set i to 3 guzinta 12 output "d" % i
This implements the guzinta
operator for integer
s. But we may also want to perform a guzinta
calculation for floating point numbers. Here is the guzinta
function for float
s:
import "omfloat.xmd" unprefixed define float infix-function value float a guzinta value float b as return b / a process local float x set x to 3.5 guzinta 15.75 output "d" % x
This works fine. But what if you want to perform guzinta
calculations on integer
s and float
s 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 i local integer left initial { 3 } local integer right initial { 15 } set i to left guzinta right output "d" % i output "%n" process local float x local float left initial { 3.5 } local float right initial { 15.75 } set x to left guzinta right output "d" % x output "%n"
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?
process local float x local float left initial { 3.5 } local integer right initial { 15 } set x to left guzinta right output "d" % x output "%n"
This code will produce a compile-time error with the message:
Operator type mismatch An argument for operator 'GUZINTA' is of the wrong type. The type signature encountered 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. A float
contains fractional information while an integer
does
not, so in the case of an operation on integer
s and float
s, 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 |
symbol 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 | integer into float) b as return b / a
However, this will not 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 x set x to 3.5 guzinta "15.75" output "d" % x
This program will produce the error:
Operator type mismatch An argument for operator 'GUZINTA' is of the wrong type. The type signature encountered is 'NUMERIC-LITERAL GUZINTA STRING'.
We are used to string
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 string
s, and
in an overloaded function, automatic conversion of these string
s to integer
s or float
s 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 string
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 string
s or numeric literals return float
s.
Here are the functions to add to our set of guzinta
functions to support string
and numeric
literal types:
; NUMERIC LITERAL ; define overloaded float infix-function value (numeric-literal into float) a guzinta value (integer | float | string | 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 ; STRING ; define overloaded float infix-function value string a guzinta value (integer | float | string | numeric-literal into float) b as return b / a define overloaded float infix-function value (integer | float into float) a guzinta value string b as return b / a
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
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 ycan be extended to create a
record
type pixel
by adding a color
field:
declare record pixel extends point field string 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 function display (value point p) as return "d" % p:x || ", " || "d" % p:y define string function display (value pixel p) as return "d" % p:x || ", " || "d" % p:y || ": " || p:color
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 function display (value point p) as return "d" % p:x || ", " || "d" % p:y
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 function display (value pixel p) as return "d" % p:x || ", " || "d" % p:y || ": " || p:color
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 p 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 p to a set new p to b repeat over p as p output display (p) 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 function display (value publication p) as return p:name || "%n" define overriding string function display (value novel n) as return 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 dynamic overloaded string function display (value publication p) as return p:name || "%n" define overriding string function display (value novel n) as output n:name || " a " || n:genre || " novel%n" define dynamic overloaded string function display (value point p) as return "d" % p:x || ", " || "d" % p:y define overriding string function display (value pixel p) as return "d" % p:x || ", " || "d" % p:y || ": " || p:color
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.
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
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 p as p output display (p) 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 p as p output p again
You can do this by writing a conversion-function
that calls a dynamic
function:
define string conversion-function value point p as return 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 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.