Methods
As discussed in Classes and Objects, a Class block can contain declaration blocks and/or method definitions. This page describes the characteristics of these class methods and their definitions. For information about defining special-purpose methods that are less broadly applicable and therefore not part of the class, see Local and Common entities.
Method definition syntax
All declaration blocks must appear before any method definitions. A method definition consists of the method header, which is basically a reiteration of the method declaration, the method body (code), and an End method statement:
methodHeader [internalNamesBlock] methodBody End methodType [methodName]
The methodHeader must nearly match the method declaration (which is described in detail in Method declarations and method types), except:
- Named parameters do not have to appear in the same order.
- The optional Public, Private, or Shared indication may be present on one and not the other.
The parameter names must be present on the method declarations (as opposed to the Declare Subroutine statement) and must match the parameter names in the method header. This redundancy may seem extreme, it has several benefits:
- It ensures that a complete method description is available in both the declaration block and the method definition. The former is convenient for users of the class and someone trying to understand the class as a whole. The latter is convenient for someone looking at the method in isolation. Meaningful parameter names on the method declarations will make the method functionality much clearer than the parameter datatype alone.
- Complete redundancy makes it possible to cut-and-paste or copy a method declaration to the method definition, or vice versa. The method body consists of the code that implements the method or methods.
Functions and Subroutines have one method, but Properties can have two methods: one for retrieving the property (a Get method), and one for setting the property (a Set method). The Get and Set methods are enclosed in Get and Set blocks:
Property name [(parameters)] [Is] type [internalNamesBlock] Get getmethodBody End Get [methodName] Set setmethodBody End Set [methodName] End Property [methodName]
The Get and Set methods can appear in any order inside the Property definition block, though must both appear within a single property definition block. ReadOnly methods have a Get method only. WriteOnly methods have a Set method only.
Comparing methods to complex subroutines
Methods behave very much like SOUL complex subroutines, and they have some important differences:
- Methods can have optional parameters, that is, parameters not specified in the method invocation.
- Method Input parameters (unless they are arrays) are passed to the method by value — by copying the corresponding argument values provided when the method is invoked, instead of working with a pointer to the argument values. This copying allows the parameters to be updated within the method without consequence to the arguments outside the method. Method Output parameters are passed by reference — pointers to the argument values are passed instead of argument value copies, and updates to the parameters within the method affect the actual arguments outside the method.
- Instance-specific (non-Shared) methods always have an implicit input parameter: the object on which they operate, also known as the method object. This implicit object can be referred to by the parameter name %this.
Note: It is not valid to declare a local variable named
%this
in non-Shared methods because it is implicitly declared by SOUL. - The Set method in a Property has another implicit input parameter: the value to which the property is being set. This parameter has the same name as the property. For example, if a property is called
Height
, the input value in a Set method for an instance-specific (non-shared) method may be referenced as:%this:height
Note: Within the Set method, the input value is copied (not referenced directly), so it may be updated.
- Methods, being part of a class, can access the Private methods and variables in a class.
- All statement labels within a method definition must be unique.
- Local variables in methods are always auto-initialized upon entry. They take either their explicit Initial values or implicit initial values by datatype (0 for Fixed and Float, a null string for String and Longstring, a null for Objects).
- Local variables are always stacked for recursive entry to methods. That is, if a method is called directly by itself or indirectly by other methods, the subsequent executions get their own copies of all local variables.
- Object variables and Longstrings are automatically cleaned up on exit from methods. Object variables are set to null, and if a variable is the last reference to an object, the object is discarded (see, Object variables). Longstrings are set to null, and any CCATEMP pages associated with the Longstrings are freed.
- Methods that return a value must have the return value indicated on the Return statement.
Examples of method definitions
Function
The following is an example of a Function that adds an amount to a private variable and returns the new value:
class account private variable balance is fixed dp 2 end private public function adjustBalance(%amount is fixed dp 2) - is fixed dp 2 callable end public function adjustBalance(%amount is fixed dp 2) is fixed dp 2 callable %this:balance = %this:balance + %amount return %this:balance end function end class ... %myAccount is object account ... %balance = %myAccount:adjustBalance(50.00)
If the application does not need the new balance resulting from the AdjustBalance
, the last line could be written:
%myAccount:adjustBalance(50.00)
As when invoking Variable members of a class, the colon (:) that separates the object variable %myaccount
and the method adjustBalance
, above, may alternatively be specified with a single blank before and/or after it.
Property
The following example illustrates a Property that returns or sets a temperature in Fahrenheit, and that uses a public variable to retrieve or set the Celsius temperature.
class thermometer public variable celsius is float property fahrenheit is float end public property fahrenheit is float get return (1.8 * %this:celsius) + 32 end get set %this:celsius = (%fahrenheit - 32) / 1.8 end set end property fahrenheit end class %temp is object thermometer %temp = new ... %temp:fahrenheit = %input ... print 'Temperature fahrenheit: ' %temp:fahrenheit print 'Temperature celsius: ' %temp:celsius
Inside a non-shared method, references to the implicit input object, %this
, can omit the “this:” and refer to public and private class variables as if they were local variables. For example, the above Fahrenheit
property could have been written as:
property fahrenheit is float get return (1.8 * %celsius) + 32 end get ... end property fahrenheit
Nevertheless, the this:
can be specified to make clear that a reference is to a class variable or to distinguish a class variable from a local variable:
property fahrenheit is float ... set %celsius is fixed dp 2 %this:celsius = (%fahrenheit - 32) / 1.8 %celsius = %this:celsius print 'Temperature changed to ' %celsius end set end property fahrenheit
Because a local variable is accessed instead of a class variable by the same name, the %this
is required to reference the class variable inside a class method. This precedence of local variables ahead of class variables means that public or private variables can be added to a class without fear of “breaking” methods that might have local variables with the same name. This might be important in very large classes with many methods. There are other cases where the %this
is required to access the method object.
Subroutine
The following is an example of a Subroutine that sets a private class variable and then displays all private variables in the class:
class comic private variable name is string len 32 variable pratfalls is fixed variable trademark is longstring end private public subroutine display(%newName is string len 32) end public subroutine display(%newName is string len 32) %name = %newName print 'Name = ' %name print 'Pratfalls = ' %pratfalls print 'Trademark = ' %trademark end subroutine display end class %stooge is object comic ... %stooge:display('Curly')
Optional and default parameters
Methods support optional parameters, that is, parameters that do not need to be passed on every invocation of the method. For example, suppose a function returns the number of items in a bin. And suppose that function can be passed a number of items to add to the bin. Usually when the function is invoked, no items are added, but once in a while, they are. So the function can be declared as follows:
class bin ... public ... function numberOfItems(%add is fixed optional) ... end public ... end class
Given this declaration, the function could be invoked as follows:
%yellow is object bin ... %yellow = new ... print %yellow:numberOfItems
It can also be invoked as follows:
print %yellow:numberOfItems()
And, finally, in cases where a value is to be passed to the method, it could be invoked as follows:
%items = %yellow:numberOfItems(%number)
When a parameter is optional and it is not passed by the invoker, that parameter has a standard value, based on its datatype, inside the method. This standard value is the standard initial value for a variable of the parameter's datatype. These initial values for each datatype are the following:
- String
- A null (zero-length) string.
- Longstring
- A null (zero-length) string.
- Float
- 0.
- Fixed
- 0.
- Object
- Null.
Note: Because specifying the Optional keyword for an Object parameter means that the absence of the parameter presents a null object parameter variable to the method, Optional implies the AllowNull keyword and, in fact, it cannot be specified along with AllowNull.
- Structure
- Each item in the structure is given its default initial value, which is the same as indicated in this list for the other datatypes, unless an Initial clause was used in a structure item declaration in the structure definition. So, if a method is defined as follows:
function square(%number is float optional) ... return %number * %number ... end function
The following will print
0
:print %object:square
For the “simple” datatypes Fixed, Float, or String, it is possible to specify an alternative value to be passed to a method for an unspecified argument. This is done by specifying the Default keyword instead of Optional. As of Sirius Mods 7.1, default values can also be specified for Enumeration parameters, including Booleans. The Default keyword must be followed by a string, numeric, or enumeration constant in parentheses:
function square(%number is float default(-1)) ... return %number * %number ... end function
And the following statement prints 1
since the square of -1 is 1:
print %object:square
The following method declaration uses an alternative default string value:
subroutine addCustomer(%lname is string len 32 - default('***Unknown***') )
The following method indicates a default value of Share
for a LockStrength enumeration parameter:
function getRecord(%key is string len 10, - %ls is enumeration lockstrength default(share)) - is object record in file foo
Optional or default parameters are allowed on any kind of method: functions, subroutines, properties, or constructors. Constructors, especially, can often benefit greatly from optional parameters as there are often extra qualifiers for newly instantiated objects that might be present, but often are not. For example, one might have a constructor for a product object that allows, but does not require, the specification of a product code:
constructor new(%productCode is string len 8 optional)
In this example, the New
function could then be invoked with or without the optional
product code:
%cake = new ... %pie = new('31415929')
Is [Not] Present test for optional or default parameter
Generally, it is sufficient for a method to simply use the replacement value for an optional or default parameter without caring whether that value was specified explicitly or generated implicitly. For example, if a method is declared as follows:
subroutine increment(%amount is fixed default(1))
It probably does not matter whether it is invoked as
%object:increment(1)
or
%object:increment
However, there may be cases where a method wants to distinguish between the two cases. For example, consider the following class:
class incident public subroutine setComment( - %comment is string len 64 optional) ... end public private variable haveComment is float variable comment is string len 64 ... end private ... end class
A null string might be a valid comment, but it also might be useful to be able to use the SetComment
method defined above to indicate that there is no comment. So, it might be necessary to distinguish the following two invocations:
%object:setComment ... %object:setComment()
To make this possible, SOUL provides the Is Present and Is Not Present tests for optional and default method parameters:
subroutine setComment(%comment is string len 64 optional) if %comment is present then %this:haveComment = 1 %this:comment = %comment else %this:haveComment = 0 %this:comment = end if end subroutine setComment
The Is Present and Is Not Present tests can be especially useful in code where the default value is not a constant. The most common example of this is a date parameter that is usually the current date, but not always. For example, consider a constructor that sets the start date for a transaction to the current date, unless an optional date is passed to the constructor:
constructor new(%startDate is string len 8 optional) if %startDate is not present then %startDate = $sir_date('YYYYMMDD') end if ...
While the same thing could probably be accomplished, in this case, by checking for a null value, it is a bit neater to check for the presence of the parameter.
Named parameters
Methods support named parameters, that is, parameters that are passed by name rather than position. For example, one might have a method called Roundabout
that has a parameter that specifies a value for an exit. It might be invoked as follows:
%bypass:roundabout('KIDLINGTON')
If the first parameter for this method were allowed to be invoked by name, this method might also be invoked as:
%bypass:roundabout(exit='KIDLINGTON')
As this example illustrates, a named parameter is indicated by the name of the parameter followed by an equal sign, which is then followed by any expression that is to be assigned to the parameter.
Named parameters are supported for both system and user-written methods.
Arguments that are passed by name are called named arguments, while those that are not are called positional arguments, because their meaning is determined by their position in an argument list. While a method can be invoked with both positional and named arguments, all the positional arguments must precede the named arguments. That is, the following is valid because all the positional arguments precede the named ones:
%mini:addPetrol(24, 'Stratford', price=89.5, brand='BP')
But the following is invalid because the named parameter litres precedes the positional parameter Stratford
.
%mini:addPetrol(litres=24, 'Stratford', price=89.5)
A name on an argument can be allowed or required. If allowed, the argument can be specified either positionally or by name. If required, the argument can only be specified by name. Whether the name on an argument is allowed or required has nothing to do with whether the argument is required. That is, just as positional parameters can be required, Optional, or Default, so too can named parameters. That said, named parameters will tend to be optional, since required parameters usually have a fixed position in a parameter list.
As noted before, system methods can have named parameters. To determine which parameters on a system method are positional, name allowed, or name required, see the documentation for that method. An example of a system method with a name required parameter is the ParseLines method, which has a name required (but optional) StripTrailingNull parameter:
%list:parseLines(%string, stripTrailingNull=false)
For user-written methods, the distinction between positional, name allowed, and name required parameters is made on the method declaration and definition header. By default, the parameters in user-written methods are positional. For example, in the following method declaration, all parameters are positional:
subroutine display(%customerId is string len 10, - %startyear is float, - %showFamily is enumeration boolean optional, - %showEmployer is enumeration boolean optional, - %showMedical is enumeration boolean optional)
So the method might be invoked as follows:
%cust:display(%crn, %year, true, , true)
To indicate that a name can be used for a parameter, the NameAllowed keyword should be specified on the parameter declaration:
subroutine display(%customerId is string len 10, - %startyear is float, - %showFamily is enumeration boolean optional, - %showEmployer is enumeration boolean optional, - %showMedical is enumeration boolean optional nameAllowed)
So that the method could then be invoked either as before, or as follows:
%cust:display(%crn, %year, true, , showMedical=true)
Note: Even though the parameter name is declared with a leading percent character (%), the name of the parameter when invoking the method should exclude the percent character.
If the NameRequired keyword is specified on a parameter definition, the parameter must always be passed as a named argument rather than a positional one:
subroutine display(%customerId is string len 10, - %startyear is float, - %showFamily is enumeration boolean optional, - %showEmployer is enumeration boolean optional, - %showMedical is enumeration boolean optional nameRequired)
If a parameter other than the last is specified with NameAllowed, all parameters after that are also treated as if NameAllowed were specified on their declarations. That is, the following two declarations are equivalent:
subroutine display(%customerId is string len 10, - %startyear is float, - %showFamily is enumeration boolean optional nameAllowed, - %showEmployer is enumeration boolean optional nameAllowed, - %showMedical is enumeration boolean optional nameAllowed)
subroutine display(%customerId is string len 10, - %startyear is float, - %showFamily is enumeration boolean optional nameAllowed, - %showEmployer is enumeration boolean optional, - %showMedical is enumeration boolean optional)
Similarly, a NameRequired keyword implies NameRequired for all subsequent parameters. A NameRequired parameter can follow a NameAllowed parameter though, of course, all parameters after the NameRequired parameter are also assumed to be NameRequired, whether or not that keyword is specified. A NameAllowed parameter cannot follow a NameRequired parameter. Named arguments can be specified in any order, so given the above declaration, the following two invocations are identical:
%cust:display(%crn, %year, showFamily=true, showMedical=false)
%cust:display(%crn, %year, showMedical=false, showFamily=true)
There are many reasons to use named parameters. One is to make method invocations easier to understand. If a method has many parameters, the invocations can look like:
%cust:display(%crn, %year, true, false)
And, if a programmer looking at this did not have the parameters for this method memorized, she would have to go to the method declaration to have an idea of what was being done here. But naming the arguments makes the code much clearer:
%cust:display(%crn, %year, showFamily=true, showMedical=false)
As this example illustrates, named parameters can be particularly useful for boolean switches that alter the behavior of a method, especially when there are many of these switches.
Another advantage of named parameters is that they are order independent. This means that place-holders do not have to be specified for optional parameters that are not specified on their invocation. This is especially important for methods with many optional parameters. For example, in the absence of named parameters, a method with seven optional parameters might be invoked as follows:
%mini:drive(,,,,,,,65)
Such a call can be made much easier to read, and much less error prone, by using a named parameter:
%mini:drive(speed=65)
Named parameters can also be useful to delay decisions about parameter order. That is, if a method has, say, two optional parameters, and it is not immediately obvious which one is most likely to be used more often (and thus be the first positional parameter), the parameters can be initially declared as NameRequired:
subroutine add(%oil is float optional nameRequired, - %petrol is float optional nameRequired)
All invocations of the method would then have to use names:
%mg:add(petrol=18)
If after some experience, it is determined that one parameter is much more commonly used than another, that parameter can be changed to a NameAllowed parameter:
subroutine add(%petrol is float optional NameAllowed, - %oil is float optional nameRequired)
When that change is made, invocations of the method can use the NameAllowed parameter positionally:
%mg:add(18)
Note that this delaying tactic can even be useful with non-optional parameters, if there is a possibility that some of the non-optional parameters might be made optional some day — perhaps after appropriate defaults are determined.
Finally, named parameters can be used to “rescue” a (retrospectively) bad decision about parameter order. For example, if after some use, it is determined that for a method with many parameters, the fifth parameter is more often specified than any of the earlier parameters, it can be turned into a NameAllowed parameter, making it possible to specify that parameter without having to specify the earlier parameters. All this said, named parameters are not a panacea. There are some drawbacks to using named parameters:
- They make method invocations more verbose, sometimes with little or no benefit. This is especially true for methods with one or two parameters where the meaning of the parameter(s) are obvious from the name of the method.
- Time and care must be taken to chose good parameter names. Once a parameter name is chosen for a named parameter, it can be quite difficult to change it, as this would require changing all code that specifies that parameter name in a method invocation.
So, as with most programming constructs, the use of named parameters is an art rather than a science. As named parameters should be used where appropriate, they should be avoided where inappropriate. But other than the vague and not inviolate principle that named parameters are more likely to be useful in methods with more parameters, the appropriateness of named parameters is a subjective matter.
Note: A final point should be made about named parameters: their syntax is somewhat of an inconsistency in User Language. In most contexts where user-written expressions are allowed, an equal sign is treated as a comparison operator.
For example, in the following statement the variables %value
and %crn
are compared, and, if equal, the global CRN
is set to 1, otherwise it is set to 0:
$setg('CRN', %value=%crn)
If %value
or %crn
were not defined, this statement would generate a compilation error.
Given this syntactic structure, one might expect that a named argument in a method invocation would be interpreted as a comparison of a field name with a value. For example, one might expect the argument to the New method to be the result of a comparison between field Host
and the variable %target
:
%socket = new(host=%target)
The reality is that the use of the equal sign is almost always limited to conditions in an If statement, so one probably wouldn't expect to find a comparison in the above context. Object-oriented SOUL takes one step further by interpreting any single token followed by an equal sign in a method parameter as a named argument, whether or not what is to the left of the equal sign is a valid parameter name. For example, the following will produce a compilation error, even if %input
is a defined variable, and even though it is not a valid parameter name:
%object:doIfTrue(%input = 1)
In the odd cases where one really wants to pass the result of an equality comparison to a method, the alternative character comparison operator can be used:
%object:doIfTrue(%input eq 1)
or, the comparison can be wrapped in an extra parenthesis:
%object:doIfTrue((%input eq 1))
Parameter description guidelines
A method declaration contains a description of the method's parameters. A parameter description includes the parameter name, type, and attributes (Optional, AllowNull, and so on). The names should be descriptive, as this makes it easier for users of a method to understand what the method does by looking at its declaration. For example, the following method declaration
subroutine fill(%petrol is float, %oil is float)
is much more helpful than this declaration:
subroutine fill(%x is float, %y is float)
With the availability of named parameters, the use of meaningful parameter names becomes even more important, because the parameter names will actually appear in the method invocations:
%multipla:fill(oil=0.5)
The InternalNames block - map argument names to names internal to the method
It is possible that the ideal names for method declarations are not the ideal names for the parameters inside the actual methods themselves. For example, one might have a standard that all parameters should start with parm.
in method code, so that it is easy to tell which variables are parameters. If this standard is extended to the names on the declaration, the result is ugly and redundant parameter names in declarations and method invocations:
%multipla:fill(parm.oil=0.5)
The InternalNames block is provided to allow mapping of parameter names on declarations to another name to be used inside the method, making it possible to use the optimal names on method declarations and inside methods, even if the two names are not the same.
The syntax of the InternalNames block is:
InternalNames newName [Is] parameterName ... End InternalNames
Where:
newName | The name to be assigned to that parameter for internal use. |
---|---|
parameterName | The name of the parameter as it appears in the method declaration. |
The InternalNames statement must be the first statement after a method definition header, which means that for properties it must appear before the Get and Set blocks.
For properties, the InternalNames block changes the names of parameters for both the Get and Set methods. InternalNames can be used to rename implicit parameters, such as:
- %this, which is used to reference the method object.
- The input value parameter for properties, which has the same name as the property (preceded by a percent).
In the following example, the input variables %petrol
and %oil
are mapped to the names %parm.petrol
and %parm.oil
, respectively:
subroutine add(%petrol is float nameRequired optional, - %oil is float nameRequired optional) internalNames %parm.petrol is %petrol %parm.oil is %oil end internalNames ... %petrol = %petrol + %parm.petrol %oil = %oil + %parm.oil
This example illustrates how if petrol
and oil
were private class variables, the InternalNames block makes it possible to have parameters in a method declaration with the same name, but to avoid naming conflict or confusion between the private class variables and the parameter names.
Stringing method invocations together
Often, a method will return an object, that is, an instance of a class. In such cases, it is possible to invoke another method against the result object by stringing together method invocations.
In the following example, the method object for the Slap
subroutine is the object returned by the Call
function:
class comic public function call(%name is string len 32) is object comic subroutine slap ... end public ... end class ... %moe is object comic ... %moe:call('Curly):slap
The same is true for stringing a method invocation to an object variable in a class:
class comic public variable brother is object comic subroutine slap ... end public ... end class ... %moe is object comic ... %moe:brother:slap
This stringing of method/variable names can be extended indefinitely and is one of the big advantages of object-oriented syntax over standard procedure syntax. Procedural syntax accomplishes the same economy using nesting:
call $slap($brother(%moe))
With procedural syntax, the evaluation is from the inside out and should be read that way. Evaluation of strung-together class members is left to right, and so it can be read in that more natural order. If there is more than one input object to a method, object oriented syntax must also resort to nesting, but this is relatively rare and still requires one fewer nested object than procedural syntax.