One use of operations is as dynamic (possibly two-way) channels on which messages can be sent and received. This mode of operation arises from the use of an in statement at the receiver. This is strictly part of concurrent MPD and will not be discussed here.
The other possible use of operations is to implement it with a proc. In short a proc provides an implementation for the operation in the form of a block of program code which gets run every time the operation is invoked. As we shall see, the combination of operations and procs is used to encode the notions of procedures, processes (remote procedure calls in general).
An operation declaration gives one or more operation definitions separated by commas.
op operation_definition, operation_definition, ... |
Each operation definition specifies the name of the operation, the types of its parameters and the type the return value (if any). The operation definition can optionally specify a restriction on the way the operation can be invoked.
operation_id operation_specification operation_id operation_specification operation_restriction |
The operation specification specifies the parameters and an optional return value.
( parameter_specification_list ) ( parameter_specification_list ) returns ret_type var_name |
The form of the parameters specification list is a (possibly empty) comma separated list of formal parameters
param_specification, param_specification, ... |
Each parameter specification specifies one or more parameters of the given type.
var_type var_id, var_id, ... |
Parameters may be arrays in which case their name is followed by the dimension. Thus, return identifiers and variable identifiers has two forms.
identifier or identifier dimension
Dimensions declarations are equal to those of variable declaration. One extra form of dimension,*, is available for parameters (not return values), indicating that the procedure is polymorph in the dimension of the array.
{call} {send} {call,send} {send,call}
|
The meaning of the different invocations is discussed below.
The following are three examples of operation declarations.
op insert(person item) op differ(string[*] s1,s2) returns int place op left(int i) returns int lft |
Parameter and return names are optional in operation declarations since the names must be specified by the proc or in. Thus, the above example could just as well be written as
op insert(person) op differ(string[*], string[*]) returns int op left(int) returns int |
However, because the name of the parameter documents its role, it is usually useful to include it. An operation can also be declared using operation types, which are shorthands for operation specifications. This is explained further below.
You can specify how individual parameters are to be passed to the procedure by preceding the type by either var, val, res and ref with the following interpretation:
A proc implementing an operation has one of two forms depending on whether the operation is declared to return anything or not. A proc is associated to an operation by having the same name as that operation. When associated to an operation without return value, a proc declaration has the form:
proc operation_id ( formal_id_list ) { block }
|
A proc tied to an operation with return value has the form
proc operation_id ( formal_id_list ) returns result_id { block }
|
The formal identifier list is a list of zero of more comma separated identifiers.
id, id, ... |
Values are returned by assigning to the variable which has been defined after the returns keyword in the procedure declaration. Assigning the return value does not terminate the execution of the procedure. Termination of execution occurs either when execution reaches the end of the body of the procedure or when a return statement is found. The form of a return statement is simply
return |
Execution of a return statement immediately stops the execution of the procedure and returns control to the caller, returning the last value assigned to the return variable.
Generally, there are three syntactic forms of operation invocation, two of which are statements and one is an expression.
call operation_id ( expr_list ) send operation_id ( expr_list ) |
The operation is the name of an operation. Usage of the send keyword results in the creation of a new process in order to run the body of the proc implementing the operation (and is thus not part of the sequential part of MPD). The expression list is a (possibly empty) list of expressions which evaluation result will be passed as parameters to the operation. The number of expressions should be equal to the number of formal parameters.
expr, expr, ... |
An operation returning a value can be used in expressions. The syntactic form of of such operation calls is formed by omitting the call keyword.
operation_id ( expr_list ) |
Consider
int a = id(10) # assign 10 to a call id(10) # call the id procedure and throw away the result send id(10) # invoke the id procedure asynchronously int l = len(arr) # calculate the length of array arr |
A command of the form
operation_id ( expr_list ) |
is simply a shorthand for
call operation_id ( expr_list ) |
An operation type declaration introduces a new name for an operation type that can be used as a synonym for the specify type of operation. Operation type can be used in operation and capability declarations. Operation type are great tools when you want to use operations of the same type more than once. An operation type declaration has the form
optype optype_id = operation_specification operation_restriction |
The specification and restriction has the same forms and meaning as described in the previous section. The = can be omitted if desired. The following three examples of optype declarations are all equivalent.
optype intfun = (int n) returns int sum optype intfun (int n) returns int sum optype intfun (int) returns int |
As mentioned above operations can be declared using optypes instead of explicitly giving the operation specification. The general form of this is
op optype_id operation_id, optype_id operation_id |
An example of this is
op intfun fact |
It declares fact to be an operation whose specification is given by intfun. The operation fact can then be implemented by a proc.
proc fact(n) returns prod {
prod = 1
for [i = 2 to n] {
prod *= i
}
}
|
Operation type can be used to make the program self documenting by intentional use of types. The also make the source text more compact. Compare the following example
op expr () returns int op term () returns int op factor () returns int |
to the following more compact version
optype ftype = () returns int op ftype expr, ftype term, ftype factor |
cap operation_id cap optype_id cap operation_specification cap operation_specification operation_restriction |
An operation capability can be bound to any operation having the same parameterization. When the parameterization is compared, only the signatures of formals and return values matter; formal and return identifiers are ignored. Capabilities can be compared for equality using the == and != relational operators. Order comparison using the other relational operators (e.g. <, etc) is not allowed. Given the following operations
op d(int x) op e(var int x) optype s = (int x) op f(real x) returns real y op g(real a) returns real b |
we can declare the following operation capabilities
cap d x #capability x pointing to operation of the
# same form as operation d
cap f y #capability y pointing to operation of the
# same form as operation f
cap s z #capability y pointing to operation of the
#same form as operation type f
|
Some ways the variables can be used
x = d # x now points to operation d
x(387) # invoke d through capability x
# with argument 387
if (...) { y = f } # make y point to f or g
else { y = g }
write(y(4.351)) # invoke whatever y points to, and
# print the result
z = x
if (y == f) { ... } # capabilities can be compared
|
However, the following statements are illegal (taking the comments into account):
x = e # the parameter type of e is var int
# but x is val int
d = e # d is an operation, you cannot assign to operations
z(3,45) # z requires a single parameter
|
A procedure which does not return a value has the form
procedure ( param_spec,param_spec, ... ) { block }
|
A procedure returning a values has the form
procedure ( param_spec,param_spec, ... )
returns ret_type var_name { block }
|
Parameter specification declares one or more variables of the same type with the possibility if specifying how the parameters are passed, just as for operation parameter declarations above. are passed:
procedure a() { # a procedure returning nothing,
# taking no parameters
...
}
procedure id(int a) returns int b { # the identity procedure
b = a # that returns its argument
}
procedure len(int a[*]) returns int b {
# a procedure that calculates
# the length of the given array
int l = 0
for [i in lb(a) to ub(a)] {
l++
}
b = l
}
|
Passing by reference (ref) is more efficient for passing large data structures.
resource main()
procedure factorial(val int x) { # "val" tag is optional
int fact = 1
for [ i = 1 to x ] { fact *= i }
write(x, "factorial is", fact)
}
# find factorial for each positive number input
int x = 10
while (true) {
if (x == 0) { exit
} else if (x < 0 ) { write("number must be >= 0")
} else if (x > 0 ) { call factorial(x)
}
x--
}
end
|
* for a range bound or a string
size. Here is an example:
resource main()
procedure sort(var int a[1:*]) {
for [ i = lb(a) to ub(a)-1,
j = i+1 to ub(a) st a[i] > a[j] ] {
a[i] :=: a[j]
}
}
int x[1:20], y[2:30], z['a':'z']
# initialize x, y and z
...
sort(x)
sort(y)
sort(z)
end
|
Note that sort has a var parameter, so the changes to the
parameter by the call have an effect on the value of that parameter. A
ref parameter could also be used here.
Here is an example involving strings. It takes a string and ``centers'' it:
resource main()
procedure center(var string[*] s) {
int fill = maxlength(s) - length(s)
int front = fill/2
s = string(([front] ' ')) || s || string(([fill - front] ' '))
}
# read in a string; center it; repeat.
string[20] str # maxlength(str) is 20
while (read(str) != EOF) {
center(str); write(str)
}
end
|
This kind of polymorphism over array sizes can be very useful. For instance consider an array indexed from 'a' to 'd'. We can use len to calculate its length.
int b['a':'d'] write(len(b)) # writes 4 on the terminal. |
The size of the arrays are completely unchecked statically! For instance, consider the following statically correct procedure that takes a one element array as input and returns a two element array
procedure runtime_fail(int a[1]) returns int b[2] {
b=a # would result in a runtime error!
}
runtime_fail([2] 1)
# would result in another runtime error!
|