Tech News Back Issues Issue: 111705
Object Classes and Instances: Function Objects
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!
(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?
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 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:
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.
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...
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.
Copyright of the text and images herein remains with the respective
author. No part of this newsletter may be reproduced, transmitted,
stored in a retrieval system or translated into any language in any
form by any means without the written permission of the author or
Omnis® and Omnis Studio® are registered trademarks, and Omnis 7 is a trademark of Raining Data UK Ltd. Other products mentioned are trademarks or registered trademarks of their corporations. All rights reserved.