Selectors
One of the most important rules in software engineering is don't repeat yourself. If you find yourself writing the same code, or almost the same code, in more than one place then that is a sign that your code smells. For instance, if you see this code
Node root = current_node;
while (root.get_parent() != null)
root = root.get_parent();
near this code
Node topmost = left_leaf;
while (topmost.get_parent() != null)
topmost = topmost.get_parent();
you should feel a strong urge to factor out the similarities:
Node find_root(Node start) {
Node current = start;
while (current.get_parent() != null)
current = current.get_parent();
return current;
}
and then just use that method:
Node root = find_root(current_node);
...
Node topmost = find_root(left_leaf);
Refactorings like this is something we do all the time: notice similarities in our code and factor them out.
But not all similarities are easy to factor out. In the example above it was easy: the same thing was done with two different objects, current_node and left_leaf. Factoring out the subject of an operation is usually easy: you just create a method or function that takes the subject as an argument. But consider these two code snippets:
Node root = current_node;
while (root.get_parent() != null)
root = root.get_parent();
and
File root_directory = current_directory;
while (root_directory.get_enclosing_directory() != null)
root_directory = root_directory.get_enclosing_directory();
These two pieces of code are almost identical in what they do but in this case they're not only different in the object they operate on but also in which method is called. In most object-oriented languages you can't "factor out" the name of a method, like get_parent or get_enclosing_directory in this example, so you can't write a find_root method that can be used to replace both loops as we could in the previous example.
In neptune, on the other hand, there is a mechanism for abstracting over method names: selectors. A selector is an object that represents the name of a method. For instance, the name of the get_enclosing_directory method is written as ##get_enclosing_directory:0
. The syntax of a selector, at least in the common case, is ##
followed by the name of the method, colon, and the number of arguments expected by the method. Given a selector object you can invoke the corresponding method on an object using the perform syntax:
Selector sel = ##to_string:0;
Point p = new Point(3, 5);
String s = p.{sel}(); // = p.to_string()
The syntax recv.{expr}(args...)
means "invoke the method specified by expr on recv with the specified arguments. Using this, the loop example from before can be refactored into
find_root(var start, Selector method_name) {
var current = start;
while (current.{method_name}() != null)
current = current.{method_name}();
return current;
}
and then the two instances can call that method:
Node root = find_root(current_node, ##get_parent:0);
...
Node root_directory = find_root(current_directory,
##get_enclosing_directory:0);
Using selectors this way can sometimes be useful but code that is identical except for the name of a method is pretty rare, at least in my experience. But selectors can be used for many other things.
One of the most useful applications of selectors is delegates. A delegate is a selector coupled with an object. You can think of it as a delayed method call: you specify a particular method to call on a particular object but you don't perform the call just yet.
Point p = new Point(3, 5);
Delegate del = new Delegate(p, ##to_string:0);
String s = del();
Here, we create an object, then we create a delegate which can be used to send to_string() to the object, and finally we invoke the delegate which causes to_string() to be called on the point. The syntax for invoking a delegate is the standard function call syntax: delegate(args...)
.
The syntax new Delegate(...)
is a bit cumbersome so there is also a binary operator, =>
, that can be used to create delegates:
...
Delegate del = (##to_string:0 => p);
...
How are delegates useful? Well, the place where I've had most use for them is as event handlers. For instance, we have a rudimentary GUI toolkit based on Qt that uses delegates for all events:
void draw_controls(qt::Widget parent) {
qt::Button ok_button = new qt::Button(parent);
ok_button.add_on_click_listener(##ok_button_clicked:0 => this);
}
void ok_button_clicked() {
System.out.println("Ok button clicked");
}
This code demonstrates how delegates can be used in a very light-weight mechanism for specifying event handlers, in this case causing the system to print a message on the console each time the button is clicked. And if we use accessor methods the code that sets the event handler can be made even more concise:
...
ok_button.on_click = (##ok_button_clicked:0 => this);
...
Another use of delegates is for spawning threads. Besides just invoking a delegate, you can also call the spawn method which invokes the delegate in a new thread:
void start_process() {
Worklist list = new Worklist();
(##produce:1 => this).spawn(list); // spawn producer
(##consume:1 => this).spawn(list); // spawn consumer
}
void produce(Worklist list) {
while (true) {
var obj = produce();
list.offer(obj);
}
}
void consume(Worklist list) {
while (true) {
var obj = list.take();
consume(obj);
}
}
The start_process method starts two threads: on one that adds objects to the worklist and one that consumes those object, again in a very light-weight fashion using delegates to invoke two local methods in separate threads. I think that's pretty elegant!
Unlike languages like C#, delegates in neptune are not "magic"; they are implemented as pure neptune code that uses selectors and perform to do the actual delegation. While selectors might not look like that useful a construct they can be used to build some very powerful abstractions.