Object Classes and Instances:
Function Objects
By David Swain
Polymath Business Systems
In the last article, I proposed
four categories of uses for Objects in Omnis Studio, but there was
not the space to give much detail on or examples of Objects belonging
to any of those categories. We will begin correcting that omission
in this article.
Briefly, let's review the concept of a Function Object. This is
an Object Class we build to contain public methods that will be
used as custom functions - methods that generally accept parameters
and return values and which we will call implicitly from within
expressions. Omnis Studio contains a great number of functions for
manipulating string, numeric, date, binary and other types of values,
but no programming environment can possibly contain all conceivable
functions. Fortunately for us, Omnis Studio was made extensible,
so we can create our own functions!
Functions
(N.B. - In some programming languages, the term "function"
is used where we use "method" in Omnis Studio. My use
of this term here - and its general use within Omnis Studio - is
more narrow.)
A function in Omnis Studio is a code segment that can
be called from within an expression to perform some manipulation
of values and return a resulting value. This resulting value may
be further manipulated within the expression because the function
itself may have been an operand of an operator or it may have been
nested as a parameter of another function. Here is a simple example:
The built-in function, sqr() accepts a single parameter,
which itself is an expression, and returns the square root
of the evaluated result of that expression. Here that result is
then multiplied by 5. Obvious, I know - but we have to begin somewhere.
A custom function is a public method that we
write using the Omnis comand language and then invoke within an
expression using Omnis Notation. Omnis Studio (not Omnis
7 or previous generations) allows us to invoke public methods using
Notation and to use such a Notation string within an expression,
just like a built-in function. Let's see what that looks
like:
Notice how we need to specify where the function lives in the application.
But otherwise, calling our custom function is no different from
calling any other function. The implications of this are huge!
The main rule is that the class to which our function code belongs
must be instantiated for us to be able to access it in
this way.
So what is the difference between a custom function and
a built-in function? Functionally, not much. They both
accept parameters and both return a resulting value. They both can
be included in expressions, although the way we include a custom
function is slightly different from the syntax of a built-in function
(in that we must qualify the name of the function with its location).
Built-in functions are black boxes, so we can't view or modify the
code they contain - but then, we aren't responsible for their accuracy
either. Built-in functions are also globally available throughout
the Omnis Studio environment, while we must decide how and where
to deploy our custom functions. This is where the concept of a Function
Object comes in.
Why Use An Object?
It is not necessary to put custom functions into Object Classes.
But doing so gives us the most flexibility in using them. Still,
there are alternatives...
If we only need access to certain functions within a single class
in an application (but from various points within the instances
of that class), we can simply use a public method of that class.
There is no need to be concerned whether the class containing the
method we need has been instantiated - or what the name of that
instance is - because it is the current instance, $cinst.
Calling it from within that instance is easy (as shown above)! Calling
it from other places in the application requires that we know the
name of the instance (and what type of instance
it is) and that the instance exist in the first place:
This could become cumbersome and awkward.
If we need global access to a method for use as a function, a good
argument can be made for making it a public method of a fixed "main"
menu of the application. If the menu will always be installed, it
is therefore always accessible - and its address will always be
$imenus.<menuname>, which should be easy enough to
remember. This is even true in multiple task applications because
it doesn't matter to which task this menu belongs - it is still
a member of $imenus. But we would still have to qualify
the function name like this:
We could shorten this a bit by defining an Item reference
variable of appropriate scope to point to the main menu. We could
then avoid some typing:
The same argument could be made for housing global custom functions
in a fixed toolbar instance that is installed on startup.
If we only ever have to build one application, the "main
menu" argument is a strong one. But if we are in the business
of building different applications for different clients, or building
framework applications for use by various developers, then portability
becomes an issue. We also may develop hundreds, or thousands, of
custom functions over time and that menu or toolbar may not be able
to hold them all - or might need only a few of them for a given
application.
So why use an Object Class for housing functions?
- First, Objects are portable. We can move them from one library
to another or we can store them in the VCS and apply them to libraries
as needed.
- Second, Objects allow us to group related functions into a single
package, but separate from other types of functions. This gives
us more flexibility by allowing us to only put those function
families needed for an application into a library.
- Finally, it allows us to chose the scope of access for a family
of functions. We may not need specialized financial functions
available on a window for entering customer address information,
for example. Using Objects in a tighter scope may help us use
RAM more efficiently (although instantiating the same Object as
an instance variable in a dozen simultaneously open windows defeats
that purpose...).
Consider this one more design option that offers its own unique
advantages and challenges.
Designing a Function
The requirements for a custom functions are:
- The method must be a public method
- The method usually should accept parameters
- The method should return a resulting value
The only time a method used as a function would not require a parameter
is if it is used to simply pass back a constant (like the msgcancelled()
function) or some system information (like the platform()
function). Most functions are used for performing operations on
values passed to them, but there are exceptions.
Let us consider a simple example. Omnis Studio has a built-in function,
rnd(), which rounds the value it is given to a specified
number of decimal places. The function requires two parameters:
the number to be processed and the number of decimal
places to be used (expressed as a non-negative integer less
than 16). This function is useful in a number pf ways, but it is
still limiting. There are many times when we might need to round
a number to some other basis than a negative power of 10. What if
we need to round to the nearest multiple of 1000... or of .05...
or of 64? A more general function is required for this.
An expression that returns a value rounded to whatever
basis we provide is as follows:
To see how to use this expression, an example would make the best
explanation. Suppose we want to round the number 32 to the nearest
multiple of 7. Number = 32 and basis = 7. The expression is evaluated
as follows:
- First, number is divided by basis (32/7) to yield an intermediate
result (approximately 4.57)
- We then add .5 to that to get another intermediate result (approximately
5.07) as a test to whether we should round up or down (assuming
the midpoint between integer multiples rounds up)
- We then take the integer part of that value (5) and multiply
it by the original basis to get our final result (35)
This also works for rounding to a certain number of decimal places,
but we have to specify the basis (.01) rather than the number of
decimal places (2). So now that we have a workable expression for
the operation we want our function to perform, we can begin building
the method.
We need to specify two parameters for our method. Both must be
numbers in this case (since we are performing a numeric operation),
but our best choice for the type of number is Floating dp,
which allows us to use the method for any combination of number
and basis we might need to handle.
Our method in this initial stage (yes, we have plans for more options
later in this article) only needs to be one line of code. We use
the Quit method command to return a value from a method
in Omnis Studio - and that's all we need for the moment. Parameter
values are automatically obtained when the method begins execution,
so all we have to do is perform the calculation as the Returns
option of a Quit method command! The code looks like this:
Sure, it's only one line of code (for the moment), but it's one
line we only have to create in one place. Putting it in an Object
Class allows us to deploy it wherever we need it, which could be
at the task, class, instance or local
level of scope.
We will name our method $rndb for "round to a basis".
As long as we remember that it requires two parameters, we should
have no problems.
Documenting a Function Object
But that brings up an interesting thought: How might we remember
how many parameters we need, and what their data types are, months
from now. More importantly for many developers: How can we communicate
this to co-developers working on the same project or to customers
who have purchased our Object(s) as part of a product? This is a
job for the Interface Manager!
There are many uses for the Interface Manager (opened either from
the View menu in the Method Editor or from the Variable Context
Menu of an Object variable). But we are only interested in one item
here: the description of a method. The creators of Omnis
Studio have set a good precedent and have given us a number of examples
of useful descriptions that we can access from this tool. For example,
look at the description of the $redraw() method for a window
class shown here:
The description first shows us the syntax for the method and its
parameters. Optional parameters are shown inside square
brackets. They even named their parameters so that the data
type of the parameter is indicated by the first letter of the
name (I don't usually do that) and the name itself indicates the
purpose of the parameter (I always try to do that).
If we need more detailed information about a parameter (like data
type), we can find that under the Parameters tab. The description
then continues to briefly explain what the method does. Descriptions
of more complex methods go on to explain the various options available.
This information is read-only for built-in methods, but we can
enter our own descriptions for custom methods. Actually, in version
4.x of Omnis Studio, we don't even have to open the Interface Manager
to annotate a method. We can do so right in the Method Editor! The
description is entered by clicking on the field between the Code
Pane and the Command Details Pane:
We can also add a description to each of the parameters of our
methods that will appear in the Interface Manager in the list under
the Parameters tab. We must add these descriptions in the Variables
Pane of the Method Editor, though.
The Interface Manager not only allows the developer to see a description
of the method (including syntax, if that is provided by us), but
it allows the name of the method, with parameters along for the
ride, to be dragged from it to an entry area in the Method Editor.
Very convenient!
Using a Function Object
Setting up a Function Object for use is merely a matter of choosing
an appropriate scope, creating a variable of object type
(we'll discuss the object reference type in a later article),
giving that variable an appropriate name (I prefer to use short
ones when possible) and assigning our Object Class containing our
custom functions as the subtype for that variable. For example,
if our custom rounding function is kept in an Object Class named
functionObject, we might create a variable of Object data
type named fn to use it. If we then need to round the current
time to the nearest quarter hour, we could use our custom $rndb()
function in our code like this:
To explain what this expression does: when #T is used numerically,
it returns the number of minutes past midnight represented by the
time value. We then convert this to the decimal number of hours
by dividing by the number of minutes in an hour. We round this to
the nearest multiple of .25 and then multiply the result of this
by 60 to return to minutes. Finally, the built-in tim()
function is used to convert the numeric result this yields back
to a time value. We could also (and perhaps more simply) cast the
expression this way:
This version of the function code focuses on rounding to the nearest
15 minutes rather than the nearest quarter hour. Sometimes thinking
about different ways of wording a problem can lead to a simpler
solution. But the point here is that the use of Function Objects
is very simple. The main challenge is to determine the appropriate
scope for deploying the Object Instance.
Perhaps a set of functions is only required in certain reports.
We would then set up an instance variable in each report that needs
to use functions from that Object Class. An instance of this class
would only exist (taking up RAM space) while one of those reports
is being processed. In the example above, I used an instance variable
of a window. Depending on the range of locations where our Function
Object might be needed, we might choose a broader (task) scope or
a narrower (local) scope.
For instance and local Object variables, there is another advantage:
If we copy the code in our example to a different class, the fn
instance variable definition will be automatically created in the
target class or method if it does not already exist there with that
name. Of course, this is a double-edged sword. Care must be taken
to name the variables we use for Function Objects consistently because
we can have more than one instance variable in the same class that
instantiates the same Object Class. They only need to have different
names.
Multi-Purpose Functions
It happens that our generalized rounding function has a couple
of close relatives that might occasionally prove useful in our work.
We'll use a few diagrams to help clarify this. There are a few "step-wise"
functions that work much like rounding. By "step-wise",
I mean that a continuous series of input values maps to the same
output value. Here is a diagram that schematically maps this function
(we assume a square grid in this diagram, but there could be rectangular
versions of it):
This graph is for rounding to integer units, but think of it as
integer multiples of a basis value. Input (parameter) values are
given along the horizontal axis and output (result) values are given
along the vertical axis. The red dots on the step-wise curve indicate
the exact value where the next step begins, but the right end of
each step only approaches (but never quite reaches) its ending value.
This is a graphical way of saying that the convention for rounding
is that we include the halfway point between two values with the
higher value.
But this is not the only way of "rounding" values. For
some purposes, it is more appropriate to round any fraction beyond
a given value up to the next unit. this is know as the
ceiling function. This function again generates a step-wise
curve, but it is somewhat different:
Grouping of items into histogram divisions or percentiles works
in this fashion. All values up to and including a given cutoff value
are included in the same group, but values even ever so slightly
past the cutoff are in the next group. (Kind of like late fees in
bill paying...) An expression that would provide this function for
any basis would be:
(N.B. - The modulus function in Omnis Studio is not an integer
function, so we can use it with fractional values as well as integers.
For simplicity, we will also restrict our ceiling function to positive
values here.)
In other situations, we might instead choose to round down
to the nearest unit. This is known as imposing the floor
function. For unit integer values, we have the int() function
in Omnis Studio for this purpose, but we may on occasion need to
impose this for a given basis (how many 2 liter bottles can we completely
fill with this quantity of product?) rather than for a basis of
1. Here is how the floor function maps out:
An Omnis Studio expression for this function might be:
(N.B. - Again, we want to avoid negative or zero values for now
- especially for the basis value.)
If we wanted to (and this is only one possibility), we could include
all three of these options in our round to a basis
function. Of course, this would require a few more lines of code...
It will also require an additional parameter - one that determines
which form of rounding is to be used for a given set of
number and basis values. Let's call this parameter
roundingType, give it a Short integer data type
and restrict it to values between 0 and 2 inclusively. We might
also decide that the most likely use of this function is the original
round to a basis (midpoint test) use and give roundingType
a default value of 0 (so it doesn't have to be supplied
when we are simply rounding).
And speaking of default values, we might also want to give basis
a default value of 1. That way we can eliminate sending the second
parameter as well if we just want to round an input number
to the nearest integer.
There are two parts to the method in its expanded form. The first
part tests parameter values and make certain they fall within the
proper range. The second then uses the Switch command and
the roundingType value to determine how to process the
other input values. Here is the completed method:
While the first section may be useful while debugging our application,
we may or may not want to include it in a finished product. Otherwise,
this function can now operate in three different modes depending
on the value of an optional third parameter.
Optional Parameters: Take Two
On some occasions, we may need to perform entirely different operations
in a function depending on the number of parameters that
are actually sent to our function method rather than on flag values
sent to specific parameters. For example, the ann() built-in
function returns a value for the annuity as a whole if only its
required first five parameters are used. But if the optional sixth
parameter (period number) is sent. it returns a value for the specified
period. All the code needs to know is whether five or six parameters
were sent.
Omnis Studio (as well as Omnis 7) has a little-known feature called
the parameter count parameter. It is a legacy from the
Omnis 7 technique of creating adhoc parameters without using the
now-obsolete Parameter command from that generation of
the product. The name of this parameter is %0. (%%0
can also be used, even though it is a Character variable.)
It is also important that this be the last parameter for
the method and that it not be sent a value. (I noticed
in my copy of Omnis Studio 4.0.3 that reordering the parameter list
can occasionally cause this parameter to not perform its intended
function and that it must be removed and recreated - in the proper
position - to regain its effectiveness. So you might find it to
be a bit "touchy"...) This parameter receives the count
of the parameters actually sent to the method - even if some unsent
parameters are still given default values. This allows us to set
default values for parameters that are optional and still detect
how many were actually sent by the calling method.
Another feature of the ann() function is that it solves
for the parameter value that is sent as a question mark ("?").
There is an intimate relationship among the five arguments of the
annuity function that makes this possible, but we may find similar
situations in our own work. If this need ever arises, we should
make all the parameters of our function method of Character
type so that we can accept a question mark or similar character
and then test for it as follows:
This is all intended to get you thinking about possible uses for
Function Objects...
Next Time
I hope you have found this article to be useful and thought-provoking.
In the next issue of Omnis Tech News, we will explore the concept
and uses of Helper Objects. |