Lightweight interfaces instead of function types

Stephen Colebourne scolebourne at joda.org
Fri Feb 19 09:28:22 PST 2010


This is an outline of an idea that has appeared before wrt function
types - lightweight interfaces.
http://www.jroller.com/scolebourne/entry/closures_lightweight_interfaces_instead_of
http://weblogs.java.net/blog/forax/archive/2008/01/yet_another_clo_1.html
The time has come to mention the concept on this list.

I'll express the "proposal" in informal language for now:

As indicated previously, function types have issues in their
interactions with arrays. Given, the lack of general reification, it
also appears that reified function types are unlikely.

By comparison, SAM interfaces have no such difficulties. You can hold
an array of SAM interfaces safely today. If the SAM interface is
generified, then there may be some associated warnings, but developers
are familiar with those, and the effects on the type system are known.

As such, the aim of this "proposal" is to achieve most (not all) of
the goals of function types via SAM interfaces.

1) Lightweight interface:

(The keyword interface is reused to save adding a new keyword)

A lightweight interface consists of the keyword interface, an
interface name and an interface method signature (minus the method
name):

  interface FileJoiner String(Reader...) throws IOException;
  interface MathsOperation int(int, int);
  interface Comparer<T> int(T, T);

A lightweight interface has a direct translation to a standard SAM,
which adds wildcards to generics:

  public interface FileJoiner extends Functor {
    String invoke(Reader...) throws IOException;
  }
  public interface MathsOperation extends Functor {
    int invoke(int, int);
  }
  public interface Comparer<T> extends Functor {
    int invoke(? extends T, ? extends T);
  }

The public/protected/package/module/private scope for the interface is
inferred by usage (if its only used in a protected method signature,
then the interface only needs to be protected, etc).

I've added a no-method functor marker interface, but that may not be required.

It may be necessary to prevent classes from implementing lightweight
interfaces (which would favour a new keyword instead of interface).


2) Usage:

The lightweight interface is used in place of a function type in a
method/variable declaration. It is expected that the lightweight
interface is defined just above the annotations on the method:

  interface Collector String(String...);
  interface Counter long(Collector, char);
  void storeHelloWorldCount(Counter counter, Collector collector) {
    database.storeCount(
      counter.invoke(
        collector.invoke("Hello", "World"), '-'));
  }

The lightweight interface can also be defined within a method body:

  public int process() {
    interface MathsCombiner int(int,int);
    MathsCombiner add = #(int a, int b) {return a + b;};
    MathsCombiner mul = #(int a, int b) {return a * b;};
    return mul.invoke(add.invoke(2, 3), add.invoke(3, 4));
  }

As a lightweight interface is effectively equivalent to a SAM
interface, the implementation of lambda expressions and blocks is
effectively similar to the creation of an anonymous inner class
(although I'll maintain my opposition to inner-class-scoped "this").
Other implementations may be possible too.


3) Conversions:

There is a boxing conversion from a lightweight interfaces to an
equivalent lightweight interfaces or SAM (but not from SAM interfaces
in general).

  interface IntTransformer int(String);
  public IntTransformer constantProvider(int val) {
    return #(String str) val;
  }
  interface IntParser int(String);
  public String format(IntParser parser, String text) {
    return Integer.toString(parser.invoke(text));
  }
  public void myApplicationMethod() {
    sysOut( format( constantProvider(6) ) );
  }

In this example, constantProvider() returns an IntTransformer, which
is passed to format() which requires an IntParser. Because the source
is a lightweight interface, it is boxed to allow it to be passed to
format(). The equals() method is defined to work as == on the
base/unboxed object.

There may be opportunity to avoid physical boxing by dynamically
adding the target interface to the source type (something that might
be a useful JVM change for dynamic languages).

Finally, its important to note that function types still exist in the
compiler (as they do today). But they only exist for validation type
conversion and boxing.


4) Arrays:

Given Neals example:

 class Ex {
   static #String()[] array = new #String()[1];
   static Object[] objectArray = array;
   static <T> void impossible() {
     final T t = null;
     objectArray[0] = #() t; // assign type #T()
   }
 }

this becomes:

 class Ex {
   interface StringProvider String()
   static StringProvider[] array = new StringProvider[1];
   static Object[] objectArray = array;
   static <T> void impossible() {
     final T t = null;
     objectArray[0] = #() t;
   }
 }

Now, this fails to compile. There is no publicly available function
type for  #() t to be represented as, thus it cannot be assigned to
Object (or into Object[]). There now needs to be a mechanism to "cast"
to a StringProvider:

     objectArray[0] = (StringProvider) #() t;

This "cast" would have enough information to either allow or deny this
at compile or runtime to generate an error or warning. (Detail lacking
here...!)


Summary
-------

Pros:
- simple to learn/understand by developers (easy to guess the meaning
without tuition)

- "Transparant" mapping to existing syntax avoiding performance issues

- Works with arrays (as far as SAMs work today)

- Provides a meaningful name (and potentially documentation) for each
part of the function type

- Avoids issues of function types that take/return function types
having very hard-to-read syntax

- Should be implementable in JDK 7 timeline

Cons:
- Requires developers to name everything

- Doesn't handle exception transparancy

- Provides only a limited amount of interaction with co-contra variance

- Cannot assign lambdas to Object without going via a suitable
SAM/lightweight interface

- Restricts implementation choices


As always, its a compromise, but one worth considering.

Stephen


More information about the lambda-dev mailing list