Classes and Objects
All entities in Sather are objects, and objects are defined by classes. Even the basic entities in Sather, such as integers and floating point values are objects in Sather. Sather has several different kinds of classes - reference classes, abstract classes, immutable classes, partial classes and external classes. The important kinds of classes are reference classes and abstract classes - the rest are used in restricted circumstances. There are also some special objects (closures) which are not directly defined by classes, but we will defer their discussion till later.
Each Sather object has an associated type which indicates the class that was used to create the object. A variable in Sather also has a type, which indicates the kinds of objects it can be assigned to.
This chapter will focus on the most common kind of classes, reference classes, and the standard constructs used to create classes. Though iterators are an essential component of Sather code, their discussion has been deferred to the next chapter, since they are a relatively novel language feature.
2.1 Preliminaries
To make it easier to present examples in the following sections, we will start by introducting a few basic classes - integers, floating point numbers and strings. We will also describe how to print out data and to use the compiler
2.1.1 Some basic classes - INT, FLT and STR
Though basic numbers and strings enjoy some special language support (such as a means to initialize them to values like 5 or "foo") , they are defined as regular classes, and are a part of the standard library.The FLT class represents floating point numbers, while the INT class represents integers and the STR class represents strings. Variables may be declared to be of any of these classes and assigned when they are declared
|
a:FLT := 3.0;
b:INT := 5;
c:STR := "foo"; |
|
It is also possible to perform the usual operations on these classes, such as addition of numbers and concatenation of strings (represented by the "+" operator
| a:STR := "foo";
b:STR := "bar"; -- + concatentates strings
c:STR := a + b; -- c is "foobar".
e:INT := 5;
f:INT := 7;
g:INT := e+f; -- g is 12
compare:BOOL := e > f;-- compare is false
#OUT + compare; -- Prints out 'false' |
|
):
Comments in Sather start with a -- and extend to the end of the line. Note that all variables have a default initial void value. For the present, void may be thought of as either the NULL pointer for reference objects, 0 for integers, 0.0 for floats and false for booleans.
2.1.2 Printing output
You can print data of various types in Sather using the command #OUT+
| a:INT := 10;
#OUT+"hello world "+a; -- Prints out "hello world 10" |
|
Treat '#OUT+' as an idiom for now; it is equivalent to the standard output routines in other languages.
2.1.3 Sather source files
Sather source files consist of lists of classes. In addition to the source files that a user specifies on the command line to the compiler, the standard library files are always implicitly examined. Definitions of the basic classes such as integers and strings as well as containers of all kinds are to be found in the standard library.
Execution of a Sather program begins with a routine named 'main' in a specified class, (a class called 'MAIN' is used by default). If main is declared to have a return value of type INT, this will specify the exit code of the program when it finishes execution.
2.1.4 Hello World
The hello world program is show below:
| class HELLO_WORLD is
main is
#OUT+"Hello World\n";
end;
end; |
|
As we mentioned earlier, printing to standard output is obtained by calling #OUT+.
If the above code is stored in the file hw.sa, it can be compiled (using the ICSI Sather compiler) by:
cs -main HELLO_WORLD -o hw hw.sa
The '-main' option simply indicates to the compiler that the main routine will be found in class HELLO_WORLD. The resulting executable, 'hw' can be run as follows
prompt> hw
Hello World
prompt>
2.2 Defining Classes and Creating Objects
Objects are usually models of conceptual or real-world entities; they consist of a combination of data, which models the state of the entity and operations which model the behavior of the entity. The body of a Sather class consists of a list of features which define the data and behavior of the class. A class defines a new type and may be used to create object instances of that type[1].
We will start by describing the data elements and then move on to the operations. In subsequent sections, we will describe the definition of object behavior in the form of routines. We will then point out that Sather provides a level of abstraction, which permits the state and behavior of the object to be treated in a uniform manner. Finally, we will describe the somewhat unusual meaning of assignment in Sather that makes this uniformity possible.
2.2.1 Defining Simple Classes
The state of a class is defined by attributes, which are have the prefix attr
| class POINT is
attr x:INT;
attr y:INT;
end; |
|
The POINT class above defines an 'x' and a 'y' attribute both of which are integers. This class is useless, as it stands, since it provides no way to create instances of itself.
Object Creation: create and new
To make objects of the POINT class, we have to introduce a create routine
| class POINT is
attr x, y:INT;
create(xvalue,yvalue:INT):POINT is
res:POINT := new;
res.x := xvalue; res.y := yvalue;
return res;
end;
end; |
|
The create routine first calls the special expression 'new'. 'new' creates a new uninitialized instance of the POINT class and returns it. All the attributes in the new instance have default 'void' values. It then assigns the 'x' and 'y' attributes of this new instance to xvalue and yvalue respectively. Instances of the POINT class can then be created as shown below
p:POINT := POINT::create(3,5);
Since creation is such a common operation, Sather provides a special shorthand for calls to the routine 'create'. The 'create' routine shown could be invoked with the # sign as shown below
point:POINT := #POINT(3,5);
Expressions using the # sign are referred to as creation expressions, and are a convenient shorthand used for creating new objects and initializing their attributes.
Attribute access
When an object of the class POINT is created, the 'x' and 'y' attributes may be accessed by 'dotting' into the object.
| a:POINT := #POINT(3,5);
-- Create a new point
#OUT + a.x ;
-- Prints out the value of 'x', which is 3
a.x := 5;
-- Sets the value of the 'x' attribute to 5 |
|
Points to note
- The semantics of a class is independent of the textual order of its class elements. In particular, the actual attribute layout used by a Sather implementation is invisible to a programmer.
- The scope of feature names is the class body
- Feature names may be either lower or upper case.
- Class names must be all upper case letters (underscores and digits are permitted except as the first character).
- The feature namespace is separate from the class namespace.
- The scope of class names is the entire program; no two classes can have the same name (unless they have different number of parameters, which will be explained in the chapter on class parametrization).
- You have to explicitly call 'new' in the create routine. The following code exhibits a common error:
| class POINT is
attr x,y:INT;
create(xval, yval:INT):POINT is
x := xval;
-- Run time error! We have no object as yet!
y := yval;
end; .... |
|
2.2.2 Checking whether an object has been created
Before a variable is assigned to an object, the variable has the void value. The expression 'void' may be used to determine whether a value is void or not
| a:POINT;
if void(a) then #OUT+"a is void!" end; |
|
. The following example will print out the string "a is void!" since a POINT is a reference class and 'a' has not been created.
| a:POINT := #POINT(3,5);
if void(a) then #OUT+"a is void!" else #OUT+"a is not void!" end; |
|
In the above version, the string "a is not void!" will be printed since an object has been assigned to the variable 'a'.
Note that the above test will not work in the same way for some of the built-in classes such as integers and booleans[2].
2.2.3 Types Introduced
Each Sather variable and object has an associated type. The type of the object indicates the class that was used to create the object. In the following example, both 'a' and 'b' have the type POINT, indicating that they are associated with instances of the POINT class.
| a:POINT := #POINT(2,3);
b:POINT := #POINT(4,5); |
|
In this example, the type of the variable 'a' is the same as the type of the object to which it is assigned. This is always the case with the reference classes we have seen so far.
When we introduce abstract classes in the chapter on Abstract Classes and Subtyping on page 79, we will see that some Sather variables can hold objects of many different types. In this case, it is useful to distinguish between the type of the variable (called the declared type) and the type of the object that it holds (called the actual type or the concrete type).
2.2.4 Hiding features: private and readonly
A fundamental feature of object oriented programming languages is that they permit an object to hide certain features which are for internal use only. Attributes may be completely hidden by marking them private. Routines may likewise be marked private, meaning that they cannot be accessed outside the original class. Attributes can also be hidden so that they can be read but not modified from outside the class, by marking them readonly.
| class POINT2 is
private attr x:INT;
-- x cannot be seen from outside
readonly attr y:INT;
-- y cannot be changed from outside
create(xvalue,yvalue:INT):POINT is
res:POINT := new;
res.x := xvalue;
res.y := yvalue;
return res
end;
end; |
|
This restricts external access to the attributes in the object
| foo is ...
-- some other piece of code
a:POINT2 := #POINT2(3,5); -- Create a new POINT2
#OUT+ a.y;
-- Prints out '5'
-- Illegal: #OUT+ a.x
-- Illegal a.y := 10; |
|
Points to note
- Privacy is on a per-class basis, rather than on a per-object basis. Thus, an object can access the private features of other objects of the same class. We actually use this fact in the create routine of the class POINT2 above. Assignments to the attributes of res are being done outside the object being returned.
2.3 Class Data: shared and const
In addition to object attributes, a class definition may also contain 'shared' data, which is shared by all the objects of that class.
2.3.1 Shared Attributes - Restricted global variables
Shared attributes are similar to object attributes, but are shared between all the instances of a class. They are essentially global variables that reside within a class namespace. They can be accessed and modified by any instance of the class. Shareds can have the same private and readonly restrictions that regular attributes have
| private shared i,j:INT;
readonly shared c:CHAR := 'x' |
|
Unlike regular attributes, when only a single shared attribute is defined, a constant initializing expression may be provided.
| shared s:STR := "name";
-- ILLEGAL shared s,p:STR := "name";
-- cannot use initializing expression if two shareds are
-- declared at the same time |
|
If no initializing expression is provided, the shared is initialized to the value 'void'.
2.3.2 Class Constants
Constants are accessible by all objects in a class and may not be assigned to - they must have an initializing expression from which their value is determined at compile time (there is an exception when no type is specified, as descrbed in the next subsection). If a type is specified, then the construct defines a single constant attribute which must be initialized to a constant expression. Constant expressions are recursively composed out of a combination of literals, function calls on literals, and references to other constants. More precisely, legal assignments are to
- a character, boolean, string, integer or floating point literal
- a void or void test expression
- an and or or expression, each of whose components is a constant expression
- an array literal, each of whose components is a constant expression
- a routine call applied to a constant expression, each of whose arguments is a constant expression other than void. This caveat is imporant, since create routines are called on void. Thus the following is illegal
| -- ILLEGAL const a:POINT := #POINT(3,3);
const a:POINT := void;
-- The only legal kind of constant POINT is void |
|
:[3]
- a reference to another constant in the same class or in another class using the '::' notation.
| const r:FLT:=45.6;
-- Reader routine is private r:FLT;
private const a,b,c;
private const d:=4,e,f
const bar:BOOL := r > 10;
-- Function call on constants
const foo:ARRAY{INT} := |1,2,4,5,6|;
-- Sather arrays are explained later
const baz ::= BAR::foz ;
-- foz must be a constant expression in foz
|
|
Integer constants and Enumerated Types
If a type specifier is not provided, then no initializing expression is required and the construct defines one or more successive integer constants. The first identifier is assigned the value zero by default; its value may also be specified by a constant expression of type INT. The remaining identifiers are assigned successive integer values. This is the way to do enumeration types in Sather. It is an error if no type is specified and there is an assignment that is not of type INT.
| const a;
-- a is of type INT and gets the value 0
const c,d;
-- c gets 0 and d gets 1
const e := 3;
-- e is also of type INT |
|
Points to note
- There must not be cyclic dependencies among constant initializers.
| class FOO is
const b:INT := BAR::c;
class BAR is
const c:INT := BAZ::d;
class BAZ is
-- ILLEGAL! const d:INT := FOO::b;
-- Introduces a cycle between b, c and d |
|
- Since constant initialization involves permits operations on the built-in types, the operations on the built-in types are designed so that no observable side-effects can occur during constant initialization.
- The prefix readonly cannot be applied to constants, since constants cannot be modified in any case.
- Due to their definition, constants are only useful for the basic classes such as numbers, booleans and characters. All other constants can only be assigned to be void!
| class FOO is
const a:BAR := void;
-- only legal value |
|
2.3.3 Accessing Class Data - the :: notation
It is possible to directly access the class data or features using the :: notation.
| class FOO is
const a:INT := 3;
private const b:INT := 5;
readonly shared c:INT := 6;
shared d:INT := 7;
attr f:INT;
create(i:INT):SAME is res:SAME := new; res.f := i; return res; end;
method1:INT is return d+a; end;
method2:INT is return f+a; end;
end; |
|
The shared and const class data can then be accessed using the :: notation
| #OUT+ FOO::a+"\n";
FOO::d := 3; |
|
When a method is called using the '::' notation, it is equivalent to calling the method on a void object. Calling a method on a void object makes sense if the feature only makes use of shared data and local state. If the method makes use of object data, a run-time error will result.
| #OUT+FOO::method1;
-- Prints out d+a = 10
#OUT+FOO::method2;
-- Tries to print out self.f+a
-- However, self (the object) is void, so trying to access 'f'
-- results in a run-time error - Attribute access of void |
|
- The usual privacy and modification restrictions are maintained
| a_copy:INT := FOO::a;
-- ILLEGAL FOO::c :=3; -- c is readonly
-- FOO::a := 7; -- a is a constant |
|
2.4 Routine definitions
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. Raise statements will be described in more detail in the chapter on Exceptions on page 131. 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; |
|
Using the return value
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.
2.4.1 Routine Arguments and Modes
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; |
|
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).
Multiple return values and out arguments
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
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 table below describes the argument modes
Mode | Description |
in | 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. |
out | 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. |
inout | 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 | Once parameters are discussed in detail in the chapter on Loops and Iterators on page 59. 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. |
in more detail:
2.4.2 Local Variables - Scoping and Shadowing
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.
Points to note
- 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; |
|
2.4.3 Routine calls
The most common expressions in Sather programs are method calls[4]. 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; |
|
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.
2.4.4 Simple Overloading - Selecting a routine to call
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 in the section on The Overloading Rule on page 91.
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' |
|
2.5 Conditional Execution
Sather supports the standard constructs for conditional execution - if statements and multi-way case statements
2.5.1 if statements
if statements are used to conditionally execute statement lists according to the value of a boolean expression. In this form, the if keyword is followed by a boolean expression, the keyword then, a list of statements and the final keyword end. When the statement is executed, the boolean expression is evaluated and if the result is true the statements in the statement list are executed. If it is false,then control passes directly to the end of the if statement.
| i:INT :=-15
if i < 0 then i:=-i end
#OUT + i; -- Prints out 15
j:INT :=15
if j < 0 then j:=-j end
#OUT + j; -- Prints out 15 |
|
It often happens that one wishes to perform a sequence of tests, executing only the statements which correspond to the first test in the sequence which evaluates to true. For example, we may want to produce a integer value 'y' from an integer value 'x' which has the shape of a triangular bump. It should be zero when 'x<0', equal to 'x' when '0<=x<100', equal to '200-x' when '100 <= x<200', and equal to '0' when 'x>=200'. This can be accomplished with a nested series of if statements:
| if x < 0 then y:=0
else
if x < 100 then y := x
else
if x < 200 then y := 200 - x else y := 0 end;
end
end; |
|
Because this kind of construct is so common and the deeply nested if statements can get confusing, Sather provides a special form for it. A series of elsif clauses may appear after the statements following the then keyword:
| if x < 0 then y := 0
elsif x < 100 then y := x
elsif x < 200 then y := 200 - x
else y := 0 end |
|
There may be an arbitrary number of such elsif clauses. Each is evaluated in turn until one returns true. The statement list following this clause is evaluated and the statement finishes. If none of the expressions is true, the statements following the final else clause are evaluated.
2.5.2 case statements
Multi-way branches are implemented by case statements. There may be an arbitrary number of when clauses and an optional else clause. The initial construct is evaluated first and may have a return value of any type.
| i:INT := 7;
switch i
when 1,2,3 then j := 3
when 4,5,6 then j := 4
when 7,8,9 then j := 5
else j := 10 end
#OUT+j; -- Prints out 5 |
|
This type must define one or more routines named 'is_eq' with a single argument and a boolean return value.
| 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;
is_eq(point2:POINT):BOOL is
-- In Sather,= is short hand for a call on 'is_eq'
return x = point2.x and y = point2.y;
end;
str:STR is return "X="+x+" Y="+y; end
end; |
|
Points can then be used in a case statement as shown below
| p:POINT := #POINT(3,4);
zero_point:POINT := #POINT(0,0);
case p
when zero_point then
#OUT+"Zero point\n";
when #POINT(1,1), #POINT(1,-1),#POINT(-1,-1), #POINT(-1,1) then
#OUT+"Unit point:"+p.str+"\n":
else
#OUT+" Some other point\n"
end; |
|
Note that the equal sign is really short hand for the routine is_eq. The case statement is equivalent to an if statement, each of whose branches tests a call of is_eq. Thus the above case is equvalent to
| if p = zero_point then #OUT+ "Zero point\n";
elsif p = #POINT(1,1) or p = #POINT(1,-1) or ... etc. then
#OUT+ "Unit point:"+p.str+"\n";
else
#OUT+" Some other point\n";
end; |
|
The expressions tested in the branches of the if statement are the expressions of successive when lists. The first one of these calls to returns true causes the corresponding statement list to be executed and control passed to the statement following the case statement. If none of the when expressions matches and an else clause is present, then the statement list following the else clause is executed
There is one difference between the case statement and the equivalent if statement. If none of the branches of an if statement match and no else clause is present, then execution just continues onto the next statement after the if statement. However, if none of the branches of the case statement matches and there is no else clause, then a fatal run-time error will result.
Points to note
- It is a fatal error if no branch matches and there is no else clause for case statements but not for if statements.
2.5.3 Short circuit boolean expressions: and and or
and expressions compute the conjunction of two boolean expressions and return boolean values. The first expression is evaluated and if false, false is immediately returned as the result. Otherwise, the second expression is evaluated and its value returned. or expressions compute the disjunction of two boolean expressions and return boolean values. The first expression is evaluated and if true, true is immediately returned as the result. Otherwise, the second expression is evaluated and its value returned.
Consider the code
| p:POINT;
if p.x > 3 then #OUT+p.x; end;
-- Runtime error if p is void |
|
The above block of code will work if p is not void. If it is void, however, the test p.x >3 will result in a runtime error, since it is attempting to dot into a void reference type. We can catch this problem by using the following piece of code, and the semantics of the short-circuit and
| if ~void(p) and p.x > 3 then
-- The ~ symbol indicates logical negation
#OUT+p.x;
end; |
|
The above piece of code will not generate an error, even if p is void. The first part of the and expression tests for whether p is void. If it is void, then the void test returns true and the not turns this into a false. The and therefore fails before trying to evaluate the dotted expression p.x.
A similar behavior can be seen with the short-circuit or statement, where the second expression is not examine if the first expression evaluates to true
| a:INT := 15;
p:POINT;
if a>10 or p.x < 10 then
-- Since a>10 is true, the second expression is not evaluated
|
|
- Note that booleans also define an and_rout routine, which does not have the same short-circuit behavior:
| if ~void(p).and_rout(p.x > 3) then
-- May generate a run-time error, when 'p' is void
-- The argument to the 'and_rout' routine (p.x) is evaluated
-- even when the first condition, ~void(p) fails.
-- Hence, if 'p' is void, p.x is still evaluated and generates a
-- run-time error (attribute access of void) |
|
2.6 Attribute Accessor Routines
The distinction between data and behavior is not as strong as has been described above. In fact, it is possible to implement a feature such that outside the class it is impossible to tell whether it is a feature or a pair of functions This section describes how this level of uniformity is achieved.
Each attribute definition adds a field to the object's state and causes the definition of a reader routine and a writer routine, both with the same name. The reader routine takes no arguments and returns the value of the attribute. Its return type is the attribute's type. The reader routine is private if the attribute is declared 'private'. The writer routine sets the value of the attribute, taking a single argument whose type is the attribute's type, and has no return value. The writer routine is private if the attribute is declared either private or readonly.
| class INTERVAL is
attr start:FLT;
-- Defines the public reader start:FLT
-- and the public writer start(FLT)
attr finish; INT;
create(st,fin:INT):SAME is
-- Create a new interval
res:SAME := new;
res.start(st);
-- Equivalent to res.start := st;
res.finish(fin);
-- Equivalent to res.finish := fin;
end;
end; |
|
Thus, the levels of privacy are defined by whether the reader and writer routines are public or private
| private attr a:FLT; -- Defines the reader, private a:FLT
-- and the writer private a(FLT);
readonly attr b:FLT; --Defines the public reader, b:FLT
-- and the private writer b(FLT) |
|
The same holds true for shared attribubtes. Each shared definition causes the definition of a reader routine and a writer routine, both with the same name. The reader routine takes no arguments and returns the value of the shared. Its return type is the shared's type.
| class FOO is
shared a:INT := 3; -- Defines a:INT and a(arg:INT);
readonly shared b:INT; -- Defines a:INT and private a(arg:INT);
...
#OUT + FOO::a; -- Prints out 3
FOO::a(4); -- 'a' is set to 4, same as FOO::a := 4;
#OUT+ FOO::a; -- Prints out 4;
FOO::a := 7;
-- 'a' is set to '7' , equivalent to FOO::a(7);
FOO::b(3); -- ILLEGAL! The writer routine is private |
|
Constants do not define a writer routine. Each constant definition causes the implicit definition of a reader routine with the same name. It takes no arguments and returns the value of the constant. Its return type is the constant's type. The routine is private if and only if the constant is declared 'private'.
| const r:FLT:=45.6;
-- Reader routine is r:FLT;
private const a,b,c;
-- Reader routine is private a:INT;
private const d:=4,e,f
const bar:BOOL := r > 10;
-- Function call on constants |
|
2.6.1 Attribute assignment
In order to achieve the unification of attribute assignment and routine calls, for attributes, assignment has to be given a meaning in terms of function calls.
By default, the assignment is syntactic sugar for a call of the routine with the same name as the attribute with the right hand side of the assignment as the only argument
| p:POINT := #POINT(3,5);
p.x := 3; -- Is syntactic sugar for p.x(3); |
|
In the above example, the assignment to 'x' is the same as calling the routine 'x' with a single argument.
Replacing an attribute by a routine
The beauty of this treatment of assignment is that an attribute in a class can later be substituted by a pair of routines. Consider a class to represent integer intervals, where we store the first and last value in the interval
| class I_INTERVAL is
-- Integer intervals
attr start:INT; -- Defines start:INT and start(INT)
attr finish:INT; -- Defines finish:INT and finish(INT)
create(start,finish:INT):SAME is
res:SAME := new;
res.start := start; -- Equivalent to res.start(start);
res.finish := finish; -- Equivalent to res.finish(finish);
return res;
end;
size:INT is return finish - start + 1; end;
-- Returns the number of integers in the interval
end; |
|
We can make calls on this class
| i:I_INTERVAL := #I_INTERVAL(3,10);
i.finish := 11;
-- Equivalent to a call i.finish(11);
#OUT+ i.finish;
-- Prints out 11
i.start := 15;
-- Equivalent to the call i.start(15); |
|
Suppose we then realize that we usually want to know the size of the interval, and rarely need to know the end point. It would then be cheaper to store the size directly, rather than computing it. The class can be changed so that we store the first and size and compute finish.
| class I_INTERVAL is
-- Integer intervals
attr start:INT; -- Defines start:INT and start(INT)
readonly attr size:INT; -- Defines size:INT and private size(INT)
-- size is readonly, since we only need size:INT in the interface
create(start,finish:INT):SAME is
res:SAME := new;
res.start := start; -- Equivalent to res.start(start);
res.size := finish-start+1; -- Store the result in res.size
return res;
end;
finish:INT is return start+size-1 end;
-- Replacement for the reader routine for 'finish'
-- Compute finish using 'start' and 'size'
finish(new_finish:INT) is size:=new_finish-start+1 end;
-- Replacement for the writer routine for 'finish'
end; |
|
All the calls described above will continue to work as before. The assignment to finish in particular will now be a call on the user-defined finish routine, instead of a call to the implicit writer routine for the attribute finish.
2.7 Static Type Inference
For the sake of convenience, Sather provides a mechanism for statically inferring the type of a variable from the context. This type-inference takes place in different situations, where the type is available from the context.
2.7.1 Creation Expressions
In a creation expression, it is tedious to have to repeat the type of a class on both sides of a creation expression and assignment. Hence, the # symbol may infer its type from the context.
| a:POINT;
a := #(3,4);
-- Equivalent to a := #POINT(3,4); |
|
2.7.2 Assignments and ::=
Type inference can also take place in a declaration, if it is combined with an assignment. Since the declared type of the right hand side of the assignment is known, its type is used as the type of the variable. This combination of declaration and assignment is extremely common in Sather code.
| a ::= 3; -- Equivalent to a:INT := 3;
p1:POINT := #POINT(3,5);
p2:POINT := #POINT(4,5);
p3 ::= p1.add(p2); -- 'p3' is of type POINT.
-- Assumes the function 'add' in POINT i.e. POINT::add(POINT,POINT); |
|
When an assignment is associated with a creation, we can make use of either form of type inference
| a ::= #POINT(3,4); -- Equivalent to a:POINT := #POINT(3,4);
a:POINT := #(3,4); -- Means the same |
|
2.7.3 Arguments to a function call
The type of the arguments to a function call are also known and can be used to infer the type of a creation expression in a call to the function.
| foo(a:POINT) is ...
foo(#(3,5));
-- The create expression infers its type
-- from the type of the argument that 'foo' is expecting |
|
This form of type inference may be used for closure creation expressions as well, which will be discussed in the chapter on Closures
| apply(arg:ROUT{INT}:INT) is ....
apply(bind(3.plus(_)); |
|
If the plus routine in the INT class is overloaded, then the appropriate routine is chosen based on the declared type of the argument to 'apply' i.e. ROUT{INT}:INT. Note that if both the 'apply' routine and the 'plus' routine are overloaded, type inference may not be able to determine the type and it might be necessary to create a temporary variable with the right type
| r:ROUT{INT}:INT := bind(3.plus(_));
apply(r); |
|
In any case, we strongly recommend that static type inference not be used in cases where confusion might result; the extra typing is usually worthwhile!
2.8 Class Parameters
We will briefly describe simple parametrized classes here so that they may be used in examples through the rest of the text. For a full description of parametrized classes, please see the chapter on Parametrized Classes.
A Sather class may have various type parameters, which are basically place holders for types which are specified when the class is actually used. This allows us to write code that is generic and can be used with a different types. By convention, these type parameters are given names like T or TP. We show below a class TUP, which holds pairs of objects. Since we would like to be able to hold objects of any types, we just specify type parameters, T1 and T2. These parameters are place-holders, which must be set to actual honest-to-goodness concrete classes when the TUP is actually used
| class TUP{T1,T2} is
-- Simple version of the library tuple class
attr t1:T1;
attr t2:T2;
create(t1:T1, t2:T2): SAME is
-- Standard create routine. Arguments use the type parameters
res ::= new; -- Using static type inference - new returns SAME
res.t1 := t1; -- The types of res.t1 and the argument t1
-- are both T1 so the assignment is legal
res.t2 := t2;
return res;
end;
end; |
|
We can now create a tuple object that holds, for instance, a pair consisting of a string and an integer:
| t ::= #TUP{INT,STR}(5,"this");
-- Create a new tuple.
-- Uses ::= to determine the type of 't'
#OUT + t.t1 + "\n"; |
|
2.8.1 Arrays
A standard parametrized class is the array class, ARRAY{T}. Arrays are explained in more detail on page 105. When an array is actually used to hold objects, the type parameter must be instantiated to indicate the kind of objects being held.
| a:ARRAY{INT} := |2,5,7|;
-- Special syntax for initializing an array with values 2,5,7
#OUT+a[1];
-- Return the second element of the array |
|
For example, arrays are used to pass in the arguments to a program into the main procedure.
| main(args:ARRAY{STR}) is
#OUT+args[0];
-- On unix, args[0] is the name of the program
end; |
|
We can hold a collection of points using an array, as follows
| a:ARRAY{POINT} := #(3);
a[0] := #POINT(0.0,0.0);
a[1] := #POINT(0.0,1.0);
a[2] := #POINT(2.0,2.0); |
|
2.9 A Running Example: Employees
We will illustrate the points made above by using a simple example, which will be something of a running example to be extended in later chapters. We will start here by defining a class 'EMPLOYEE'. Please bear in mind that this example is used to illustrate various language features, not object-oriented design.
EMPLOYEE definition
The class is composed of several attributes which hold the employee information. Various degrees of privacy are illustrated
| class EMPLOYEE is
private attr wage:INT;
readonly attr name:STR;
attr id:INT;
const high_salary:INT := 40000;
create(a_name:STR, a_id:INT, a_wage:INT):SAME is
res ::= new;
res.id := a_id;
res.name := a_name;
res.wage := a_wage;
return(res);
end;
highly_paid:BOOL is return wage >= high_salary; end;
end; |
|
Note the use of the special type SAME as the return type of the create routine, which denotes the current class name. SAME changes to mean the including class when it is included, as will be explained in the next chapter on code inclusion.
TESTEMP definition
The employee class may be exercised using the following main class.
| class TESTEMP is
main is
john:EMPLOYEE := #EMPLOYEE("John",100,10000);
peter:EMPLOYEE := #EMPLOYEE("Peter",3,10000);
john.id := 100; -- Set the attr "id" in john to 100
#OUT+ john.name+"\n"; -- Prints "John"
#OUT+ peter.id+"\n"; -- Prints "3"
end;
end; |
|
Note that the following calls would be illegal:
| #OUT+john.wage+"\n"; -- ILLEGAL! "wage" is private
john.name := "martha"; -- ILLEGAL! "name" is readonly. |
|
A distinguished class must be specified when a Sather program is compiled (the default is to look for a class called MAIN). This class must define a routine named 'main'. When the program executes, an object of the specified type is created and 'main' is called on it.
Running the example
To run the above example - type the code into a file emp.sa and then run the executable 'emp'
cs emp.sa -main TESTEMP -o emp
This generates the executable "emp", using the "main" routine in TESTEMP as its starting point. You can browse the resulting code by calling
bs emp.sa -main TESTEMP
[1] This is only true for reference, immutable and some kinds of external classes. Abstract a, partial and most external classes cannot have instances.
[2] The void test returns true for all integers with a value of 0 and booleans with a value of false. In general, the void test is not useful for immutable classes.
[3] Implementation Note: The compiler currently does not always detect this illegal case
[4] 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.