Skip to main content

Announcement - "No Closures" prototype

Posted by brucechapman on March 8, 2008 at 1:39 AM PST

All the major closures proposals now have prototype implementations available. But until we can play with the final option "No Closures" we're not in the best position to make a good decision. So it is with pleasure that I announce the availability of a prototype for "No Closures".

With this jar file and JDK 6, you can effectively cast a method to a SAM type. For those not following the various closures proposals too closely, a SAM type is a "Single Abstract Method" type. That is, a class or an interface with only one method that is abstract.

So to cast a method to a SAM type, the method should have the same argument types and return type as the SAM, but it can have any name. It may NOT be private. You then annotate that method with @net.java.dev.rapt.anon.As, and specify the SAM type.

Here is a method declared to be castable as a Runnable

import net.java.dev.rapt.anon.As;

class MyClass {
    @As(Runnable.class)
    void slowStuff() {
        ... // whatever
    }
}

In order to cast that method to a Runnable, you do this

new Thread(Runnables.slowStuff(this)).start();

Thats right, you get the Runnable by calling a static method on the Runnables class, and pass the object that owns the method.

 

So how does this work?

In the jar file containing the annotation there is also an annotation processor. For each SAM type with at least one corresponding @As annotation in a package, it creates a class in that package whose name is the SAM type name with an "s" appended, hence Runnable SAM type -> Runnables.java created. That class has static methods which make anonymous inner classes that delegate to each of the methods in that package annotated with @As(Runnable.class)

Those generated static methods look like this

class Runnables {
    private Runnables() {}

    static Runnable slowStuff(final MyClass owner) {
        return new Runnable() {
            public void run() {
                owner.slowStuff();
            }
        };
    }
}

If you want to pass additional arguments to the method which are not part of the SAM, just annotate those with @As.Additional, and when you call the generated class, pass those arguments.

Example

Here is a small swing app that demonstrates things.

package asdemo;

import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.lang.reflect.InvocationTargetException;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import net.java.dev.rapt.anon.As;

public class Main extends JFrame {

    JButton goButton;
    JLabel  label;
   
    public Main() {
        super("@As");
    }

    public static void main(String[] args) throws Exception {
        Main instance = new Main();
        // run instance's startUp() method on the EDT
        EventQueue.invokeAndWait(Runnables.startUp(instance));
    }
   
    @As(Runnable.class)
    void startUp() {
        setLayout(new GridLayout(2,1));
        goButton = new JButton("Go");
        goButton.addActionListener(ActionListeners.onGo(this));
        label = new JLabel("Gone");
        add(label);
        add(goButton);
        pack();
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setVisible(true);
    }
   
    @As(ActionListener.class) // ActionListener for goButton
    void onGo(ActionEvent e) {
        goButton.setEnabled(false);
        // run goBackground() in a background thread
        new Thread(Runnables.goBackground(this)).start();
    }
   
    @As(Runnable.class)
    void goBackground() {
        for(int i = 10; i >= 0; i--) {
            try {
                // call showStatus(i) on EDT
                EventQueue.invokeAndWait(Runnables.showStatus(this, i));
                Thread.sleep(500);
            } catch (InterruptedException ie) {
                return;
            } catch(InvocationTargetException ite) {
                 ite.printStackTrace();  
            }
        }
    }
   
    @As(Runnable.class)
    void showStatus(@As.Additional int value) {
        if(value != 0) {
            label.setText("Going " + value);
        } else {
            label.setText("Gone");
            goButton.setEnabled(true);
        }
    }
}

This should compile with JDK 6. You will find the generated Runnables and ActionListeners classes whereever it is you tell javac to place generated classes (the -s option if used, otherwise in the same place as the compiled .class files).

What's Hot and What's Not

This is designed to be work correctly even if you do incremental compiles. The processor remembers all the SAMs for the project and keeps them up to date, even if you only compile classes one by one.

Currently this doesn't work for generic SAM types. That means you can't use it to cast a method int compareStringLengths(String s1, String s2) to a Comparator<String>. I am working on a version that will infer the type parameters and the SAM type returned from the generated class will have the correct type parameters, but that isn't ready yet.

I don't think this is as good as any of the closures proposals, except in one respect, you don't have to wait for JDK 7 to use it.

Related Topics >>

Comments

This is an excellent suggestion. Much simpler than any of the proposals - including my own ( http://www.artima.com/weblogs/viewpost.jsp?thread=182412 ). I look forward to seeing the source.

Presumably an @Shared annotation could be added to allow write access to a local variable.

NO I can't see a way to implement @Shared. That would involve accessing a local variable in another method, or passing the variable by reference. You can't do that in Java 6 as far as I know, at least not without some gnarly bytecode manipulation. Also many of the useful use cases involve situations where the @As method is executed later and the code that "cast" the method to a SAM is no longer on the stack. However because the @As method is normally in the same class as the method that casts it, it can access fields. Alternatively you could achieve the same effect as @Shared by using the @As.Additional mechanism to pass a Future to the method. -Bruce

Thanks for the link to the Netbeans wiki on how to get this setup so that things just work. I can't over express How much I really like this Idea and solution to the "No Closures" needed approach.

When I first saw this I really liked the idea a lot. Now that I've had time to digest it I like it even more. Like others have stated I'm really looking forward to seeing the source for this. On a side note when using the supplied jar under Netbeans 6.1 the generated java file gets put in the build directory verses the source directory so the IDE will not build the application correctly. If I manually move the generated java file all is fine. Also the combination of this and JSR-296 will really rock. The Netbeans Wiki has information on how to set up a project to work with an annotation processor. Its not too hard, but it would be nice if it was a no brainer. I have filed a tree of bugs to have that and related issues addressed. - Bruce

Any updates on when you will get around to releasing the code for this? I'm still very interested in taking a look at it.

Cool!

From looking at your implementation it seems that one cannot use the @As with private methods. Have I missed something?
You missed the fact that the method is called from an inner class which is defined in the generated class in the same package. Thats why it doesn't need to be public, but cannot be private. - Bruce

It's so wonderful that after sixty years of evolution in programming languages, we can finally use C-like function pointers in Java... hooray!! Please gimme longjmp() too, in the next release. </sarcasm> Isn't that what throw new RuntimeException() does? - Bruce :)

Hi Bruce,
I like this idea, and it works like a charm. It's a pity you didn't publish the source code - it would make a good tutorial on writing annotation processors! The source code is coming, I just jumped the gun while the topic was hot - Bruce I don't really see why @As.Additional is necessary, though. Appending additional parameters to the generated helper method should be the default behaviour without the need to annotate them with @As.Additional. Yes you could do it that way. I made them explicit because it makes it easier to check that the method matches the SAM, and being explicit means the documentation is simpler. A previous incarnation of this concept worked for multiple method interfaces, and once you understood how to do it, it was quite nice, but I just couldn't make the documentation simple. So I dropped the feature. - Bruce

I agree with fabriziogiudici, just the extra 's' may cause some problems...

I like this idea. It's actually a somewhat pimped method reference solution rather than closures. The nice part is the additional parameter approach, which makes any variable to be used within the method (i.e. where a closure would close over) an explicit parameter and still make the method match the SAM.

really cool, let's hope the news spread! looking forward for the next feature release :-)

As a minor remark, probably just appending the 's' for the generated class is too risky to clash with an existing class name. What about generating a RunnableSAM instead? It's less sexy, but also less risky to clash. The generated class is in the same package as the annotated method, so the naming clash is the responsibility of the user. I thinks it's easily avoided. - Bruce