The behavior of a class is specified by routines in the class body. Routines may take arguments and may return a value.
class CALCULATOR is attr running_sum:INT; create:CALCULATOR is res:CALCULATOR := new; res.running_sum := 0; return res; end; add(x:INT):INT is res:INT := running_sum + x; return res; end; end; |
A routine definition may begin with the keyword 'private' to indicate that the routine may be called from within the class but is not visible from outside the class. The methods that are visible from outside the class are referred to as the class interface.
The body of a routine is a list of statements, separated by semicolons. In a routine with a return value, the final statement along each execution path must be a return statement. Thus, the following is not legal:
scale_x(x:INT):INT is -- Illegal routine - the else clause has no return value if x > 0 then return 15; else #OUT + "Error!"; -- last statement on this branch is not return end; end; |
A raise statement raises an exception, and can be used wherever a return statement might be required. (See unnamedlink for more details) For now, we merely note that the following version of the routine 'scale_x' does not return a value in the second branch of the if statement, but raises an exception instead, which is perfectly legal.
scale_x(x:INT):INT is if x > 0 then return 15; else raise "An error occurred!"; end; end; |
Note that, unlike most other languages, Sather forces you to make use of the return value. This may be considered an extension of strong typing - the presence or absence of a return value is a part of the signature that should not be ignored.
new_x:INT := scale_x(15); -- Legal, the return value used scale_x(15); -- ILLEGAL! Return value unused |
The return value can also be used as part of an expression.
a := scale_x(15) + 3; |
The arguments to a routine are specified as a comma-separated list. Each argument must provide a name and type. The types of consecutive arguments may be declared with a single type specifier.
create(x,y:INT):POINT ... |
The scope of method arguments is the entire body of the method, and also shadows methods and attributes in the class. If a routine has a return value, it is declared by a colon and a specifier for the return type. You can get around this restriction by using the self expression explicitly
class POINT is attr x,y:INT; add_x(x:INT) is self.x := self.x + x; end; end; |
Each argument also has a mode which determines how that argument is treated when the routine is called. If no mode is explicitly stated, the argument mode is in. That means it is simply a value sent into the routine. The other possible modes are out, inout and once (which will be described in the section on iterators).
An out argument is really like an extra return value. An out argument is not set when the routine is called; rather, it is filled in by the routine itself. Consider an integer division function that returns both the divident and remainder of the two integer arguments
divide(x,y, out dividend, out remainder:INT) is -- Note that the 'INT' type specifier applies to multiple -- arguments while the mode qualifiers apply to only one -- argument. dividend := x/y; -- Integer division result remainder := x - y*(x/y); -- Remainder after the division. -- Could also use x.mod(y) end; |
The divide routine may be used as shown below:
a:INT := 15; b:INT := 10; div, rem:INT; -- These are defined but not assigned divide(a,b,out div, out rem); #OUT + "Divident=" + div + " Remainder=" + rem + "\n"; -- Prints out "Divident=1 Remainder=5" |
Note that the out argument has to be marked both where the method is defined (i.e. as a marker of the formal parameter) and at the point of call, or the compiler will complain (once and in arguments need not be mentioned at the point of call)
inout arguments are a combination of in and out arguments. They take a value into the function and return a value out of the function. We can thus write the swap function compactly as:
swap(inout x, inout y:INT) is tmp:INT := x; x := y; y := tmp; end; a:INT := 5; b:INT := 10; -- a and b have an initial value swap(inout a,inout b); #OUT + "a=" + a + " b=" + b; -- Prints "a=10 b=5" |
The list below describes the argument modes in more detail:
All arguments are 'in' by default; there is no 'in' keyword. 'In' arguments pass a copy of the argument from the caller to the called method. With reference types, this is a copy of the reference to an object; the called method sees the same object as the caller.
An 'out' argument is passed from the called method to the caller when the called method returns. It is a fatal error for the called method to examine the value of the 'out' argument before assigning to it. The value of an 'out' argument may only be used after it has appeared on the left side of an assignment.
An 'inout' argument is passed to the called method and then back to the caller when the method returns. It is not passed by reference; modifications by the called method are not observed until the method returns (value-result).
Once parameters are discussed in detail in the chapter on Loops and Iterators (See unnamedlink). Only iterators may have 'once' arguments. Such arguments are evaluated exactly once, the first time the iterator is encountered in the containing loop. 'once' arguments otherwise behave as 'in' arguments, and are not marked at the point of call.
Declaration Statements are used to declare the type of one or more local variables. The scope of a local variable declaration begins at the declaration and continues to the end of the statement list in which the declaration occurs. Local variables shadow routines (including the accessor routines of attributes) in the class which have the same name and no arguments.
... in the POINT class ... swap_x_y is temp:INT; temp := x; x := y; y := temp; end; |
Within the scope of a local variable it is illegal to declare another local variable with the same name.
Local variables are initialized to void when the containing method is called.
Local variables are not re-initialized when the declaration is encountered in the flow of control. This is particularly relevant in loop statements, which are discussed in the next chapter. The integer 'a' is initialized to zero when the function 'compute' is entered. It is not initialized every time through the loop.
compute is loop 3.times!; a:INT; a := a + 3; #OUT + a + "\n"; -- Prints out successively 3, 6, 9 end; end; |
Note that explicit initialization (in this case 'a:=15' ) is performed every time it is encountered
compute is loop 3.times!; a:INT := 15 a := a + 3; #OUT + a + "\n"; -- Prints out successively 18, 18, 18 end; end; |
The most common expressions in Sather programs are method calls[1]. A routine call usually takes the form of a 'dotted' expression such as a.foo(b). The object on which the routine is being called ('a' in this example) is determined by what precedes the dot. If no object name precedes the 'dot', the self object i.e. the current object, is assumed. We use the following definition of the POINT class to
class POINT is attr x,y:INT; create(x,y:INT):POINT is res:POINT := new; res.x := x; res.y := y; return res; end; add(xval,yval:INT):POINT is xsum:INT := x + xval; ysum:INT := y + yval; res:POINT := #POINT(xsum, ysum); return res; end; offset_by(val:INT):POINT is return add(val,val); -- short for 'return self.add(val,val);' end; end; |
[1] We use the term 'method' here to indicate that the same description is applicable to both iterators, which have not yet been introduced, and routines.
illustrate different kinds of routine calls
If nothing precedes the method name, then the form is syntactic sugar for a call on self If the method name is preceded by an expression and a dot '.', then the method is called on the object returned by the expression. In the following example, pair (3,7) is first added to p1 and the pair (4,9) is added to that result. Note that the intermediate point that is created after the first 3,7 is added is not accessible from any variable and will be garbage collected.
p1:POINT := #POINT(3,5); p2:POINT := p1.add(3,7).add(4,9); |
If the method name is preceded by a type specifier and a double colon '::' it is presumed to be a call on a void object of the specified class (POINT in the case below)
a:POINT := POINT::create(3,5); |
This works for the create routine, since it creates a new object, res, and then makes use of it. However, this will not work for a call on, say, add
res:POINT := POINT::add(4,7); -- Runtime Error! |
Since xsum := x + xval; is actually equivalent to saying xsum := self.x + xval; the routine accesses self, which is void and cannot be accessed.
Sather supports routine overloading. We will present a simplified version of the overloading here, as it applies to the simple reference classes we have discussed. The full overloading rule will be described in more detail lateron (See unnamedlink).
Two routines in a class may have the same name provided they differ in at least one of the following aspect:
the number of arguments
the presence or absence of a return value
the type of one of the arguments (provided the types are not abstract).
Here are some examples of properly overloaded routines
foo(a:INT, b:INT); foo(a:INT); -- Different number of arguments foo(a:INT,b:INT):INT; -- Has a return value |
All of the above routines could co-exist in a single class interface. The right one would be selected at the point of call. The following two routines, however cannot co-exist in the same interface
foo(a:INT,b:INT):INT; -- foo(a:INT,b:INT):BOOL -- differs only in return type, cannot overload 'foo' |