Annotations

[ Start > PikeDevel > Annotations ] [ Edit this Page | Show Page Versions | Show Raw Source ]


Pike 8.1 adds a new feature called "Annotations". Annotations are a way to add metadata to various components of Pike code. These pieces of metadata can then be retrieved and used for various purposes. Pike includes a (growing) set of annotations that have special meaning to the pike compiler, and are used to alter program behavior and enforce constraints on the pike code you write.

Additionally, you can create your own annotations and add them to your pike code. Finally, Pike provides a number of methods that can be used to query programs and objects for any metadata attached to various components.

Annotation Syntax

Every program or identifier (such as a method or variable) within a program may have zero or more annotations attached to them. Additionally, each argument of a function may be annotated.

The syntax for adding an annotation to a program looks like this:

@some_constant_expression;

Where "some_constant_expression" is a constant pike expression. A constant expression can be a literal value like "my_annotation" or an object created from a class that has been marked as constant.

For example:

class foo {
  @"my_first_annotation";
}

To specify multiple class level annotations, simply repeat the process. For example:

class foo {
  @"my_first_annotation";
  @"another_cool_annotation";
}

While string literals are easy to use as a means of testing this functionality, using objects has obvious advantages. For example, we can create a class to be used as an annotation, and allow its meaning to be changed by passing constructor arguments. An example of this is the system provided annotation "Implements", which is located in the module Pike.Annotations. This annotation specifies that the class to which this annotation is applied will implement some interface, specified by another program. The compiler will then ensure that this is the case during compilation.

For example:

class bar {
  string gazonk();
}

class foo { @Pike.Annotations.Implements(bar);

// If we don't implement the method gazonk, the compilation will fail thanks to the "Implements" annotation. string gazonk() { return "whee!"; } }

The syntax for annotating identifiers (such as variables and methods) within a program is slightly different.

 @some_constant_expression: someItem …

For example:

class bar {
  @"my_variable_annotation":
  string default_value;

@"my_method_annotation": void easy_method() {} }

More than one annotation may be added simply by stringing the single annotation syntax together:

class bar {
  @"my_variable_annotation":
  @"another_variable_annotation":
  string default_value;
}

Now, let's try a practical example. We'll use the "Override" annotation that's included in the Pike.Annotations module. The compiler checks methods marked with this annotation to ensure that they override a method inherited from a parent class. For example:

class bar {
  string gazonk() { return "whee!"; }
}

class foo { inherit bar;

// a more excited version, apparently. @Program.Annotation.Override: string gazonk() { return "whee!!!"; } }

Were the method gazonk() not present in bar, the compiler would let us know that gazonk in the foo class wasn't actually overriding anything.

It's important to understand that annotations, on their own, don't do anything. They provide a mechanism that allows you to attach some piece of metadata to some pike construct. Some code must look at the annotation and decide to do something (or not) based on the metadata. During various stages of compilation and execution, Pike looks for the presence of some of the various system defined annotations and alters its behavior accordingly. As another example, a data parsing framework could provide a set of annotations that it would use to determine what variables in an object data should be stored. The framework would then check the annotations on a class or on individual elements of an object and use that information to tailor its behavior.

Creating new annotations

Now that we see how to annotate various components of Pike code, let's try our hand at creating a new annotation type. Imagine that we are building a data rendering system, DataMat, that can take a Pike object and turn it into a properly formatted string, according to some imaginary data format. It might be nice to have a way to specify that a variable in an object should not be included in the output. An annotation could provide a way to indicate that to the data renderer. We'll call this annotation "Exclude" and put it in the (theoretical) DataMat.Annotations module:

DataMat.pmod/Annotations.pmod/module.pmod:

protected class _Exclude {
  inherit Pike.Annotation;
  @constant;
}

constant Exclude = _Exclude();

A few items to note here:

First, we've inherited Pike.Annotation, which is a base class that provides some useful functionality for advanced annotations. It's not necessary here, but it's a good habit inherit this base class in all of your custom annotations.

Second, we've specified the @constant annotation on this class, which tells the compiler that this class produces constant objects. Recall above that annotations must be constants. If we don't mark a class with the constant annotation, the compiler will refuse to let us use it in any subsequent annotations.

Third, we're employing a bit of fanciness in order to make this annotation work more nicely for end users. Because we don't really need this annotation to accept arguments, we've created an instance of the annotation class that anyone can use. This saves the user from having to add the constructor parentheses every time they want to use it. While we could have just let the user specify it as a class, and the annotation syntax would permit that, it preserves the informal contract that "real" annotations are objects.

Let's apply that new annotation to a variable in a class:

class sample_thing {
  string name;

@DataMat.Annotations.Exclude: string nickname; }

Had we, instead, wanted to have the annotation accept a parameter (the way that Implements does), we could do it like this:

protected class Description(string label) {
  inherit Pike.Annotation;
  @constant;
}

And this could be used with the nearly-identical syntax:

class sample_thing {
  string name;

@DataMat.Annotations.Description("A person's preferred name, such as Nick"): string nickname; }

When inspecting the annotation (see below), it will have a label attribute with the given string.

Accessing metadata at runtime

Pike provides two functions that can be used to examine annotations associated with programs and identifiers. They are Program.annotations(), which queries a class for annotations applied at the class level, and predef::annotations(), which returns the annotations on identifiers located within a class or object.

Let's look at how we can query a program to see what annotations, if any, are present:

class a { }

> Program.annotations(a); Result: 0

The result of zero (0) lets us know that there are no annotations applied to this class. Let's add one to the class and see what changes:

 class a {
   @"program_annotation";
}

Result: (< /* 1 element */ "program_annotation" >)

Here, we can see that the result is now a multiset containing the list of annotations.

Examining the annotations attached to identifiers within a program is only a little more complicated.

class a {
  string x;

@"annotation_y": string y;

@"annotation_z": @"another_annotation": string z; }

annotations(a()); Result: ({ /* 3 elements */ 0, (< /* 2 elements */ "another_annotation", "annotation_z" >), (< /* 1 element */ "annotation_y" >) })

Okay, that's interesting information, we can see that there's an element matching the number of identifiers, but how can we tell which is which? Luckily, predef::annotations() works just like predef::indices() and predef::values(). An array is returned with one element for each identifier, in a stable order. So, we can combine the indices with the annotations and create a mapping out of them, for easy reference:

object example = a(); 
mkmapping(indices(example), annotations(example));
Result: ([ /* 3 elements */
              "x": 0,
              "y": (< /* 1 element */
                  "annotation_y"
                >),
              "z": (< /* 2 elements */
                  "another_annotation",
                  "annotation_z"
                >)
            ])

Now, we can easily see that identifier "x" has no annotations, "y" has one and "z" has two. That's all well and good, but how about we come up with a practical example? Thinking back to our theoretical data rendering system, we can write the beginning of some code that will use annotations to modify its behavior.

string render(object obj) {
  string output = "";

// first, let's get the name and annotations associated with each item in our object. array fields = indices(obj); array meta = annotations(obj);

// now, we can loop through each item and produce the output. foreach(fields; int pos; string identifier) { // for brevity in this example, we will skip any item that isn't a string or integer. mixed item = obj[identifier];

if(!(stringp(item) || intp(item))) continue;

// great, we have either a string or integer. // now, let's look at the annotations for the item to see if we should include it or not.

// the annotations array is ordered to match that of the indices. // if there is metadata for the item we're looking at, and one of the pieces of metadata // is the Exclude annotation, we can skip it. if(meta[pos] && meta[pos][DataMat.Annotations.Exclude]) continue;

else output += (identifier + "=" + item + "&#110;"); }

return output; }

Note that because we exposed a singleton instance of the Exclude annotation, it's simple to check to see if it's there. If we were dealing with an annotation that could have many possible instances, we would probably need to check the class using object_program() or some other mechanism to identify whether the annotation was something we cared about.

Let's try it out:

class sample_thing {
  string name;

@DataMat.Annotations.Exclude: string nickname; }

object example = sample_thing(); example->name = "William"; example->nickname = "Bill";

render(example); Result: "name=William&#92;&#92;n"

Some final notes:

Just as methods and variables are included when a class inherits another, so too are annotations: a class that inherits a class with annotations will also have those annotations. If you need to know which annotations are not inherited, Program.annotations() accepts a second argument that, if present, will return only those annotations applied directly to the program supplied to the method.

It is possible to attach the same annotation to an element more than one time. In this situation, the multiset returned by the annotation query methods will reflect this by including the element more than once.

Using predef::annotations() on a program will only display the annotations for constants contained within the class. To see the annotations for a non-constant identifier, you need to pass an object instance instead, just as with the indices() and values() methods.

The functionality provided by Pike annotations is functionally similar to annotations in Java or attributes in C#. Python has a feature called "decorators" which has somewhat similar outward appearance but is conceptually quite different.


Powered by PikeWiki2

 
gotpike.org | Copyright © 2004 - 2009 | Pike is a trademark of Department of Computer and Information Science, Linköping University