Forwarding Methods Calls

Chapel 1.15 includes a preview version of a method forwarding feature. This feature allows a record or class to specify that certain method calls will be forwarded to a particular expression. The most typical use case is to forward methods to a particular field.

Why Forwarding?

While Chapel supports inheritance between classes as a way to reuse method implementations, inheritance is not always appropriate. In particular, inheritance has the drawback that it affects the types of the classes and therefore changes what users can do with those classes. Due to some of these drawbacks, there is a well-known design pattern composition over inheritance.

The basic strategy for using composition instead of inheritance is to declare a member field that stores the type that would be inherited from. Then, declare methods that forward to the member field. This can allow the implementation to be more flexible and re-usable. In particular, changing how many classes participate in the implementation of a particular type need not change its public interface or impact the users of that type.

Besides these issues, composition is a more general strategy in Chapel due to the following language design decisions:

  • records don’t support inheritance.

  • multiple inheritance for classes is not supported.

  • inheriting a record from a class or a class from a record is not supported.

Composition can help in each of these cases where inheritance cannot apply.

Example of Forwarding

Suppose, for example, MyCircleImpl is a Chapel class, but the author of that type wishes to present it as a record (say, MyCircle). Such a division allows the type author to control what happens when the type is copied and to manage the deletion of the class instances.

Consider the following example:

class MyCircleImpl {
  var radius:real;
  proc area() {
    return pi*radius*radius;
  }
  proc circumference() {
    return 2.0*pi*radius;
  }
}

record MyCircle {
  var impl: MyCircleImpl;

  // forwarding methods
  proc area() {
    return impl.area();
  }
  proc circumference() {
    return impl.circumference();
  }
}

Since writing these forwarding methods is repetitive and boring, it’s a good candidate for automation. The Chapel language allows automating the creation of these forwarding methods by using the forwarding keyword. For example, the following is equivalent to the previous MyCircle record.

record MyCircle {
  forwarding var impl: MyCircleImpl;
}

The forwarding keyword instructs the compiler to forward method invocations on a MyCircle that would otherwise not resolve to the field impl.

Using forwarding

Besides the forwarding var style, the forwarding syntax can be applied to forward to a particular expression. For example, one might wish to generate a different error in the event that impl is nil:

record MyCircle {
  var impl: MyCircleImpl;

  proc getImplOrFail() {
    if impl == nil then
      halt("impl is nil");
    else
      return impl;
  }

  forwarding getImplOrFail();
}

var empty = new MyCircle(nil);
empty.area(); // halts with "impl is nil"

Another direction that a user of forwarding might go is that they might decide to only forward certain methods. For example, the following are equivalent ways to forward only the area() method, assuming that the only methods of MyCircleImpl are area() and circumference().

record MyCircle {
  var impl: MyCircleImpl;

  forwarding impl only area;
}
record MyCircle {
  var impl: MyCircleImpl;

  forwarding impl except circumference;
}

As with use, forwarding supports comma-separated only and except lists.

Additionally, note that multiple forwarding declarations can be specified.

Resolving Forwarded Methods

Forwarded methods resolve only when regular methods on a particular type do not resolve. For example, in the above cases, if MyCircle declared a area() method that could be called with no arguments, that method would be called in preference to the forwarded method on MyCircleImpl that is available. Other than that, the forwarded methods participate normally in the function resolution process. As with other ambiguous function declarations, ambiguity errors will be reported at the call site.

Note that special IO methods such as writeThis, readThis, and readWriteThis are not forwarded since the compiler generates these for the type by default. Similarly, a field accessor will not get forwarded if the class defines a conflicting field or method name. Consider the example below:

record ArrayWrapper {
  var array: [1..0] int;

  proc shape {
    return 1;
  }

  forwarding array;
}

The shape method above will resolve, meaning array.shape will not be forwarded.