Introduction to Ceylon Part 12

Posted by    |       Ceylon

This is the final installment in a series of articles introducing the Ceylon language. Note that some features of the language may change before the final release.

This article was updated on 2/6/2011 to reflect changes to the way annotation constraints are defined. The comment thread reflects information in the first version of article.

Annotations

If you've made it this far into this series of articles, you've already seen lots of annotations. Annotations are so important in Ceylon that it's extremely difficult to write any code without using them. But we have not yet really explored what an annotation is.

Let's finally rectify that. The answer is simple: an annotation is a toplevel method that returns a subtype of ConstrainedAnnotation. Here's the definition of a some of our old friends:

shared Deprecated deprecated() {
    return Deprecated();
}
shared Description doc(String description) {
    return Description(description.normalize());
}
shared Authors by(String... authors) {
    return Authors( from (authors) select (String name) (name.normalize()) );
}

(Note that the third example uses the syntax introduced in this blog entry.)

Of course, we can define our own annotations. (That's the whole point!)

shared Scope scope(Scope s) { return s; }
shared Todo todo(String text) { return Todo(text); }

Since annotations are methods, annotation names always begin with a lowercase letter.

Annotation arguments

When we specify an annotation with a non-empty parameter list at a program element, we need to specify arguments for the parameters of the annotation. Just like with a normal method invocation, we have the choice between a positional argument list or a named argument list. We could write:

doc ("The Hello World program")

or:

doc { description="The Hello World program"; }

Likewise, we could write:

by ("Gavin", "Stephane", "Emmanuel")

or:

by { "Gavin", "Stephane", "Emmanuel" }

But with annotations whose arguments are all literal values, we have a third option. We can completely eliminate the punctuation, and just list the literal values.

doc "The Hello World program"
by "Gavin" 
   "Stephane" 
   "Emmanuel"

As a special case of this, if the annotation has no arguments, we can just write the annotation name and leave it at that. We do this all the time with annotations like shared, formal, default, actual, abstract, deprecated, and variable.

Annotation types

The return type of an annotation is called the annotation type. Multiple methods may produce the same annotation type. An annotation type must be a subtype of ConstrainedAnnotation:

doc "An annotation. This interface encodes
     constraints upon the annotation in its
     type arguments."
shared interface ConstrainedAnnotation<out Value, out Values, in ProgramElement>
        of OptionalAnnotation<Value,ProgramElement> | SequencedAnnotation<Value,ProgramElement>
        satisfies Annotation<Value>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {
    shared Boolean occurs(Annotated programElement) {
        return programElement is ProgramElement;
    }
}

The type arguments of this interface express constraints upon how annotations which return the annotation type occur. The first type parameter, Value, is simply the annotation type itself.

Annotation constraints

The second type parameter, Values, governs how many different annotations of given program element may return the annotation type. Notice that ConstrainedAnnotation has an of clause telling us that there are only two direct subtypes. So any annotation type must be a subtype of one of these two interfaces:

  • If an annotation type is a suptype of OptionalAnnotation, at most one annotation of a given program element may be of this annotation type, or, otherwise
  • if an annotation type is a suptype of SequencedAnnotation, more than one annotation of a given program element may be of this annotation type.
doc "An annotation that may occur at most once at 
     a single program element."
shared interface OptionalAnnotation<out Value, in ProgramElement>
        satisfies ConstrainedAnnotation<Value,Value?,ProgramElement>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {}
doc "An annotation that may occur multiple times at 
     a single program element."
shared interface SequencedAnnotation<out Value, in ProgramElement>
        satisfies ConstrainedAnnotation<Value,Value[],ProgramElement>
        given Value satisfies Annotation<Value>
        given ProgramElement satisfies Annotated {}

Finally, the third type parameter, ProgramElement, of ConstrainedAnnotation constrains the kinds of program elements at which the annotation can occur. The argument to ProgramElement must be a metamodel type. So the argument Type<Number> would constrain the annotation to occur only at program elements that declare a subtype of Number. The argument Attribute<Bottom,String> would constrain the annotation to occur only at program elements that declare an attribute of type String.

Here's a couple of examples I copied and pasted straight from the language spec:

shared interface Scope
        of request | session | application
        satisfies OptionalAnnotation<Scope,Type<Object>> {}
shared class Todo(String text)
        satisfies SequencedAnnotation<Todo,Annotated> {
    shared actual String string = text;
}

Reading annotation values at runtime

Annotation values may be obtained by calling the toplevel method annotations() defined in the language module.

shared Values annotations<Value,Values,ProgramElement>(
               Type<ConstrainedAnnotation<Value,Values,ProgramElement>> annotationType,
               ProgramElement programElement)
           given Value satisfies ConstrainedAnnotation<Value,Values,ProgramElement>
           given ProgramElement satisfies Annotated { ... }

So to obtain the value of the doc annotation of the Person class, we write:

String? description = annotations(Description, Person)?.description;

Note that the expression Person returns the metamodel object for the class Person, an instance of ConcreteClass<Person>.

To determine if the method stop() of a class named Thread is deprecated, we can write:

Boolean deprecated = annotations(Deprecated, Thread.stop) exists;

Note that the expression Thread.stop returns the metamodel object for the method stop() of Thread, an instance of Method<Thread,Void>.

Here's two more examples, to make sure you get the idea:

Scope scope = annotations(Scope, Person) ? request;
Todo[] todos = annotations(Todo, method);

Yeah, everything's set up so that annotations() returns Scope? for the optional annotation type Scope, and Todo[] for the sequenced annotation type Todo. Nice, huh?

Of course, it's much more common to work with annotations in generic code, so you're more likely to be writing code like this:

Entry<Attribute<Bottom,Object?>,String>[] attributeColumnNames(Class<Object> clazz) {
	return from (clazz.members(Attribute<Bottom,Object?>))
	        select (Attribute<Bottom,Object?> att) (att->columnName(att));
}

String columnName(Attribute<Bottom,Object?> member) {
    return annotations(Column, member)?.name ? member.name;
}

As you can see, Ceylon annotations are framework-developer-heaven.

Defining annotations

We've seen plenty of examples of annotations built into Ceylon. Application developers don't often define their own annotations, but framework developers do this all the time. Let's see how we could define an annotation for declarative transaction management in Ceylon.

Transactional transactional(Boolean requiresNew = false) {
    return Transactional(requiresNew);
}

This method simply produces an instance of the class Transactional that will be attached to the metamodel of an annotated method or attribute. The meta-annotation specifies that the annotation may be applied to methods and attributes, and may occur at most once on any member.

shared class Transactional(Boolean requiresNew) 
        satisfies OptionalAnnotation<Transactional,Member<Bottom,Void>> {
    shared Boolean requiresNew = requiresNew;
}

Now we can apply our annotation to a method of any class.

shared class OrderManager() {
    shared transactional void createOrder(Order order) { ... }
    ...
}

We could specify an explicit argument to the parameter of transactional using a positional argument list:

shared transactional (true) 
void createOrder(Order order) { ... }

Alternatively, we could use a named argument list:

shared transactional { requiresNew=true; }
void createOrder(Order order) { ... }

We won't need to use reflection in our example, since Ceylon's module architecture includes special built-in support for using annotations to add interceptors to methods and attributes.

Interceptors

An interceptor allows frameworks to react to events like method invocations, class instantiations, or attribute evaluations. We don't need to write any special annotation scanning code to make use of interceptors. Ceylon handles this for us at class-loading time.

All we need to do is have our Transactional class implement the interfaces MethodAnnotation and AttributeAnnotation:

shared class Transactional(Boolean requiresNew)
        satisfies OptionalAnnotation<Transactional,Member<Bottom,Void>> &
                  MethodAnnotation & AttributeAnnotation {
        
    shared Boolean requiresNew = requiresNew;
    
    doc "This method is called whenever Ceylon loads a class with a method
         annotated |transactional|. It registers a transaction management
         interceptor for the method."
    shared actual void onDefineMethod<Instance,Result,Argument...>(OpenMethod<Instance,Result,Argument...> method) {
        method.intercept()
                onInvoke(Instance instance, Result proceed(Argument... args), Argument... args) {
            if (currentTransaction.inProcess || !requiresNew) {
                return proceed(args);
            }
            else {
                currentTransaction.begin();
                try {
                    Result result = proceed(args);
                    currentTransaction.commit();
                    return result;
                }
                catch (Exception e) {
                    currentTransaction.rollback();
                    throw e;
                }
            }
        }
    }
    
    doc "This method is called whenever Ceylon loads a class with an attribute
         annotated |transactional|. It registers a transaction management
         interceptor for the attribute."
    shared actual void onDefineAttribute<Instance,Result>(OpenAttribute<Instance,Result> attribute) {
        attribute.intercept()
                onGet(Instance instance, Result proceed()) {
            if (currentTransaction.inProcess || !requiresNew) {
                return proceed();
            }
            else {
                currentTransaction.begin();
                try {
                    Result result = proceed();
                    currentTransaction.commit();
                    return result;
                }
                catch (Exception e) {
                    currentTransaction.rollback();
                    throw e;
                }
            }
        }
    }
    
}

The intercept() method registers the interceptor - a kind of callback method. Again, we're using the syntax discussed here.

Conclusion

I think this is probably a good place to end this series for now, since I've covered all of the bits of Ceylon that have been properly worked out at this stage. At some stage I'll go back and update / clean up these posts and perhaps even reorganize the material a bit. Thanks for following along, and please keep letting us know your feedback!


Back to top