The Trumpet release introduces Announcements into Essence#. Announcements are used to implement the Observer design pattern: An observer subscribes to announcement types from a specific announcer, which is an observed object that sends out announcements of changes to its state or of other events it detects.
The Trumpet release also introduces MessageSends (used by Announcements,) which conceptually are objects that send a specific message with specific message arguments to a specific receiver. As implemented in Essence#, they also serve as polymorphic inline caches, since method lookup is only done once for each combination of selector and type (class) of receiver.
The Trumpet release can be downloaded from the Essence# site on CodePlex (which gets you the installer.) Alternatively, the code for the run time system (written in C#) can be obtained from theEssence# CodePlex site using Git, and the code for the Essence# Standard Library can be obtained from GitHub. And the example scripts can also be downloaded from the Scripts repository on GitHub.
The Essence# implementation of Announcements is conceptually the same as in other platforms, but it is not identical. The initial implementation does not use weak references or ephemerons, because those have not yet been implemented in Essence# (both are coming soon, though.) It also does not use threads, and is not thread safe, since Essence# does not currently support multiple threads (which will also be coming soon.) Also, both the Announcer class and the Announcement class use traits (TAnnouncer and TAnnouncement, respectively) to implement most of their behavior.
The core architectural roles involved in the Announcement framework are the announcer, the annoucement, and the announcement subscriber. There are also additional architectural roles for the announcement subscription and the announcement registry, but those are best viewed as implementation details that other implementations might not use, or whose API might change substantially.
An announcer corresponds to the observed object in the Observer Design Pattern. It publishes (“announces”) events (“announcements”) so that any objects that have expressed interest in (“subscribed for notifications of”) any such events (announcements) will receive notifications that an event has occurred (e.g., that an announcement has been promulgated or published.)
An announcement encapsulates / encodes / represents an event, change or news item published by an announcer. It must be an instance of a class that implements the Announcer protocol (as codified by the TAnnouncer trait.) The implementation of announcements as instances of Announcement classes is deliberately analogous to the way instances of Exception classes encapsulate / encode / represent errors and other exceptional conditions.
An announcement subscriber subscribes to and receives announcements (notifications of events) published by one or more announcers. Interestingly, there is no specific protocol to which announcement subscribers must conform. As part of the announcement subscription protocol, each one defines its own individual protocol that will be used to notify it that an event has occurred (that an announcement has been made.)
The required protocol for an announcement is actually quite small:
An announcement class must respond to the message #handles: anAnnouncement with a Boolean value: true if the message argument qualifies as an announcement of the same type as the instances of the receiving class, and false otherwise. By default, an announcement qualifies as of the same type if it responds with true to the message #isKindOf: anAnnouncementClass, where anAnnouncementClass is the receiver of the message #handles: anAnnouncement. But that implementation is not required.
Both an Announcement class and an Announcement instance must also respond to the message #asAnnouncement. When #asAnnouncement is sent to an Announcement class, the result must be an Announcement object for which the expression TheAnnouncementClass handles: anAnnouncement would evaluate to true, if the resulting announcement instance were the message argument and the class receiving the message #asAnnouncement were the receiver of #handles:. When #asAnnouncement is sent to an Announcement instance, it must answer itself.
Any class that is a subclass of Announcement can be used as an announcement, provided it does not override the required Announcement protocol methods in ways that break the Announcements API. Alternatively, any class can also serve as an Announcement class by simply using the trait TAnnouncement (the metaclass must use the trait TAnnouncement classTrait.)
Announcement classes can–and often should–add whatever behavior or state makes sense or is needed to fully represent whatever event or condition they will be used to announce.
To announce an event (publish an announcement,) the message #announce: AnAnnouncementOrAnnouncementClass must be sent to an announcer. Usually, an announcer would send that message to itself–but that is not a requirement. The argument to #announce: can be either an Announcement instance or an Announcement class. If it’s an Announcement class, the announcer will create an instance of the class that will be the object that actually gets delivered to any subscribers.
There are two different modalities for subscribing to the announcements published by an announcer: By requesting that a specific message be sent to the subscriber (the preferred method) or by requesting that a block be invoked.
To subscribe to an announcer’s published announcements by having a specific message sent to an object (which serves in the role of the subscriber,) send the message #when: anAnnouncementClass send: aSelector to: anObject, as in the following examples:
| announcer | announcer := MyAnnouncer new. announcer when: VolumeFullCondition send: #signal to: VolumeFullException. announcer when: PropertyChanged send: #invalidateProperty: to: canvas. announcer when: KeyPressed send: #respondToKeyPressed:from: to: aController.
Note that the message selector that will be sent to the subscriber may require zero, one or two arguments. If it requires zero arguments, then it will be sent to the designated receiver (announcement subscriber) with no arguments. If it requires 1 argument, then it will be sent to the designated receiver with an argument whose value is the Announcement object published by the announcer. If it requires 2 arguments, then it will be sent to the designated receiver with 2 arguments, the first of which will be the Announcement object published by the announcer and the second of which will be the announcer that published the announcement.
To subscribe to an announcer’s published announcements by having a specified block (or MessageSend instance–see below) evaluated, send the message #when: anAnnouncementClass do: action for: aSubscriber to the announcer, as in the following examples:
| announcer | announcer := MyAnnouncer new. announcer when: SomeEvent do: [:subscriber | subscriber handleSomeEvent] for: aSubscriber. announcer when: SomeEvent do: [:subscriber :event | subscriber handleSomeEvent: event] for: aSubscriber. announcer when: SomeEvent do: [:subscriber :event :announcer| subscriber handleSomeEvent: event from: announcer] for: aSubscriber.
Note that the block that will be evaluated in response to the specified type of announcement may have either one argument, two arguments or three arguments. The first argument will always the the object that serves the role of the subscriber, which must be the value of the argument following the “for:” keyword. The second block argument (if it accepts one) will always be the announcement instance (“the event”) published by the announcer. The third argument–if the block accepts/requires a third argument–will always be the announcer that published the announcement.
You might have noticed that the protocol for using blocks is different than is commonly found in other class libraries that use Vassily Bykov’s Announcement module. The reason for that is because such blocks are a common cause of memory leaks, and forcing the subscribing object to be passed in as a parameter encourages those using blocks to subscribe to announcements to identity a subscriber object other than some block, so that that subscriber object can be used to unsubscribe from the announcer. Although that’s especially critical when it’s not possible to use weak references and/or ephemerons, it matters even when weak references/ephemerons are used in the implementation of Announcements, because it would almost always be the case that a block is the worst object to use as the nominal subscriber.
To unsubscribe from an announcer, send it the message #unsubscribe: aSubscriber, with a current subscriber as the message argument.
There is a new example script named Announcements.es that provides executable example code. See the documentation of the ES command line tool for instructions on running the example scripts.
Although Essence# supports the #perform:with: suite of messages (#perform:, #perform:with:, #perform:with:with:, etc.), the use of the #perform: messages has drawbacks and limitations:
- Using one of the #perform: messages to send a message dynamically is much slower than using code generated by the compiler to send the same message statically. In other words, “object perform: #doSomething” is much slower than “object doSomething.”
- If the selector that is the argument of the perform: keyword happens to be one that a) is always or conditionally inlined by the compiler (e.g., #==, #ifNil:, #whileTrue:), or b) is implemented as a virtual method by the dynamic binding subsystem (e.g., #+, #<, #ensure:, and also many messages sent to instances of native CLR types,) then there is no actual method with that selector that can be performed.
MessageSends solve both problems. They do that by actually compiling the sending of the message named by the selector into a CLR dynamic method whenever a new selector is assigned to the MessageSend instance (which usually only occurs once when the MessageSend object is initially configured.) So when a MessageSend is evaluated, it simply invokes the dynamic method it compiled with the message receiver as the first argument, along with any other arguments as required by the selector. And the dynamic method it compiles makes use of all the same binding magic and message inlining as would be applied to any other code generated by the Essence# compiler–so you get the exact same behavior and semantics as if you had written the message send explicitly using static code. You also get the performance advantages of the DLR CallSite object which acts as a polymorphic inline cache.
So in effect, the cost of using a MessageSend to send a message is only one additional message send and one additional native CLR function call. There’s literally no additional overhead. And that’s why one of the future enhancements to Essence# will be the inlining of the #perform: messages using the same concept (and perhaps just using MessageSends themselves, as is.) One consequence of that will be that “anInstanceOfACLRType perform: #doSomething: with: anArgument” will work even in the case of native CLR methods (currently, it would not, unless a method with that selector had been added to the method dictionary of the Essence# class that shadows that CLR type.) Another will be that using #perform: would be just as “performant” as using a block.
Creating/configuring a MessageSend
As usual, a MessageSend can be instantiated by sending the message #new to the class MessageSend. But that’s not the only way.
One can also send any of the following messages to the class MessageSend: #selector:, #receiver:selector:, #selector:arguments: and #receiver:selector:arguments: (although that’s not the full list.) Alternatively, one can can just instantiate an instance using “MessageSend new” and then send one or more of the following mutator messages to the instance in order to configure it as desired: #receiver:, #selector: and/or #arguments:. Whether it’s a message being sent to the class or to an instance, the argument to the #receiver: keyword must be the intended receiver of the message, the argument to the #selector: keyword must be the message selector (name), and the argument to the #arguments: keyword must be an array of the default arguments to the message (which are optional, as explained below.)
There are also messages that can be sent to other objects that will result in a new MessageSend instance, as demonstrated in the following example code:
| ms1 ms2 ms3 ms4 | ms1 := #halt asMessageSend. ms2 := #initialize asMessageSendWithReceiver: Announcer basicNew. ms3 := (String compiledMethodAt: #asLowercase) asMessagSend. ms4 := (Collection compiledMethodAt: #collect:) asMessageSendWithReceiver: Collection selectors.
When initialized from a Method instance, a MessageSend need not and does not compile a new dynamic method. It just uses the same one encapsulated by the Method instance. That provides even better performance than using a block. The downside is that it also doesn’t check whether or not the method and the receiver are compatible.
In order to be functional, a MessageSend instance must have been assigned a selector. Whether either of the other two attributes (receiver and arguments) are required depends upon how the MessageSend instance is used.
The protocol for invoking a MessageSend is a subset of the protocol for invoking blocks: All the variations of the #value: messages are supported (#value, #value:, #value:value:, etc.), including #valueWithArguments:. However, what is not currently supported are all other other block messages, such as #ensure:, #ifCurtailed:, #on:do:, #whileTrue:, etc.
Note, however, that sending #value to a MessageSend is a special case: The effect of sending #value will be to send the specified selector to the specified receiver, using the specified default arguments. If the number of default arguments that were specified for the MessageSend instance does not match the number of arguments required by the specified selector, an error will result. An error will also result if no default arguments were specified, unless the number of arguments required by the specified selector is zero.
For all the other variations of the #value: message, any default arguments that were specified will simply be ignored, although an error will be raised if the specified selector requires a different number of arguments than are supplied by whichever variation of the #value: message is sent to the MessageSend instance. In other words, the following expression will result in an argument count mismatch error (because the specified selector requires/accepts zero arguments, but the variation of #value: sent to the MessageSend provides 2 arguments):
(MessageSend receiver: 'Hello, world' selector: #asLowercase) value: 1 value: 2
One can also use the #sendTo: message to apply the message (selector) specified by a MessageSend to an arbitrary receiver other than the one nominally specified by/for the MessageSend instance, as in the following example:
| ms | ms := #initialize asMessageSend. ms sendTo: Announcer basicNew.
Using #sendTo: has no effect on the receiver nominally specified by the receiving MessageSend instance.