||Defining Functions ||Optional Type Checking ||Optional Arguments ||Tail Recursion ||Closures ||Lazy Evaluation ||Nested Functions ||Method Aliases ||Features Index ||

User-level Functions in Omega

Defining Functions

Functions come in two (2) styles in Omega - regular objects on databases and anonymous functions. The former are declared as single statements in the form
function foo(a=1, util.Vector b) {
  a = b.size()-1;
  return(b.elementAt(a));
}
or using the more generic assignment operator
 x = function(a,b) {
    ..
 }
The latter of these illustrates anonymous functions - the right-hand-side of the assignment is an anonymous function. Anonymous functions are treated as simple components of different types of expressions and can be specified as arguments to functions, etc. For example
 vector.apply(function(x) x.mean());
(assuming vector has an approprite apply() method).

As with S and R the return value of a function is provided by an explicit return expression or is taken as the value obtained by evaluating the last expression of the function's body.

Note that in declaring a function in Omega, one can optionally specify the type of the return value. If it is omitted, not type checking is performed and the canonical java.lang.Object is assumed. The benefits of providing the type information include

Again, the optional type-checking supports the different levels of programming, providing flexibility and support/error checking by the system when desired.

Exceptions

As in Java, functions can throw exceptions and these can be optionally declared in a throws clause.

Type Checking Arguments

One of the potentially unique and powerful features (see also Dylan, I believe) of Omega is the ability to optionally type objects: variables, parameters, return values from functions, etc. can all be qualified. In addition to a simple type, all of these object instances can be qualified with a final or const keyword which makes it read-only.

Any function parameter can contain type information in the form of a class or primitive specification and arrays (which are not handled yet, I believe!) For example, the function definition

  function foo(util.Vector x, int y, z) {
    n = x.size()
    int tmp = n+10;
    local bar;
  }
has three paramters, 2 of which are constrained to be of a particular type. The evaluation model will take care of throwing an exception if the types don't match. In the case of the paratemer z, there is no type so any Object will suffice. The benefits of this include improved performace in compiled code and simpler error checking. Instead of examining the class of an argument or attempting to invoke methods within try-catch blocks, we use the system to guarantee a type and give information to the user about the inconsistencies if they arise.

Note that in the body, we can optionally type local variables (tmp and bar are local). In the case of tmp, any future assignments to it will enforce that type. In the case of bar, there is no type information but we are declaring a local variable of that name in this evaluation frame. This distinguishes it from any other variable of the same name (bar) in other databases in the search path. Without this, another instance of this variable name might be overwritten by an assignment within this scope. It is always preferrable (for understandability and reduction of odd bugs) to declare local variables.

Return values of functions can also be type checked.

  util.Vector function foo(a,b) {
    x = ...;
    return(x);
  }
When this function is evaluated and returns, the class of the the return value is checked against the declared type of the function. If they are not compatible, an exception is thrown. The default or implied type declaration is Object so that any type of object may be returned.

Again, this feature aids in debugging in addition to allowing semantic analysis on code to provide automated parellism, compilation, etc.

Optional Arguments

There is support for default values for arguments.
 function foo(x = 1, util.Vector y = new Vector(10), z = foo(x))
Note that the default value of z in our example is not overly clear. Will the value of x in the call to foo be found in this calls frame or in the callers frame? This is open for debate.

Closures

Closures are now supported. These are specified by using the keyword static as a qualifier to variable(s) declaration(s) that are to be considered persistent across invocation of the function. Other than this, the same specification and rules that apply to functions are in effect for closures. The parser and supporting classes take care of instantiating an object of the appropriate class. The evaluation is done using two databases/frames - one for the arguments as in a function and a second that stores the persistent variables. For example,
function foo(a,b) {
  static local numCalls = 1;
  static Vector z = new Vector();
    ..
   numCalls++;
    ..
}
Initialization of the static variables is done just once, when the function is first invoked. Accordingly, different sessions may have different version of the function. We might change this in the future to evaluate the objects when the function is defined. This is possible using the default evaluator returned from the
evaluator manager that is accessible through the global variables.

Tail Recursion

There is prelimary (but working) support for tail-recursive functions in Omega. I suggest that we try to use the keyword this inside a function to refer to a function of the same name but perhaps with different arguments. In that way, we can check for that name in a method call or a match against the name of the function. The use of this works for anonymous functions as well as regular named function objecs.

Nested Functions

Functions can be nested. For example,
 z = function() {
   // not called
 }

 foo = function(x) {
         z = function(a,b) {
     	  a + b;
         }
        z(x,1)
      }
Calling foo() now will lead to the local version of z being invoked rather than the global one.

The scoping rules are still up for debate. Either the S or R versions are easy to implement. At present, the basic scoping rules apply so the search path is searched from top down. This includes all the frames attached by earlier function, List, loop-bodiers, etc. evaluations. Compilation issues will help to determine the scoping rules.

Method Aliases

An expression something like
   Math.random
appears like a field acccessor, but resolves to a method acccess. Rather than returning this as a simple Java Method object, we wrap it as an evaluable object of class StaticMethodAlias or MethodAlias This can then be used in the following manner
  f = Math.random
  f()
and for non-static methods
  f = x.plot
  f(v1, v2)

In addition to using these method objects as convenient aliases, the addition also allows one to extract methods and examine it to see what arguments it takes, what it returns, etc. In other words, it allows us to easily examine the native Java classes.

If the accessor resolves to numerous overloaded methods, then we return a Vector of the StaticMethodAlias or MethodAlias objects.

ANTLR

We are using ANTLR to do the parsing. It comes with a Java 1.1 grammar and offers a nicer mechanism for specifying lexer and parser rules than lex and yacc. John (with further modifications by me) has extended the grammar to add support for functions A wonderful feature of the ANTLR material is that it generates methods for each rule rather than a simple case statement as part of a switch statement. The benefits of this include simple class extensions (see DataScanner), local variables within rules and easier debugging.
Duncan Temple Lang <duncan@research.bell-labs.com>
Last modified: Wed Jul 28 08:21:30 EDT 1999