Let us take a look at the execution model of Slate. In some languages such as Python or Smalltalk, the basic object behavior is as follows: you send a message to an object, and that object selects, using its class hierarchy, the corresponding method to execute. If none is found, then an exception is raised. Other objects you pass as arguments to the method do not play any role in the selection of the method to execute. If the method executes, you get another object in return as the result of the execution (or possibly an exception). That kind of method selection is called single dispatch, as a single object is used to look for the method to execute.
In contrast, Slate supports a multiple dispatch scheme. Instead of sending a message to a single object, you send it to a tuple of objects. Intuitively, the tuple consists of the object that would have been the receiver of the message in a single dispatch scheme, followed by all the objects that would have been the arguments to the method, in order. The objects in the tuple collectively select the method to execute, using rules that we will describe in a moment.
Roles are an important and unique feature of Slate. The definition of roles is not quite simple, although in practice, the use of roles together with multiple dispatch is quite intuitive.
Let us learn roles from an (everyday ;-) ) example. Suppose we have the following line of Slate code: archer aimAt: dragon with: arrow.
The symbols #archer, #dragon and #arrow stand for already existing objects. The signature of the method is #aimAt:with: (remember that symbols in Slate start with a pound sign). If you found the sentence: "The archer aims at the dragon with the arrow" in a novel, you might want to draw a parallel between the roles of the various actors (archer, dragon and arrow) in the action and the grammatical function of the corresponding noun in the sentence.
Slate statements are much simpler than English sentences, and always built after the same model (a subject/verb/complements pattern). Therefore, the role of any object in the action is given by its rank in the statement. In our example:
the role of the object archer is: "to come first in the call to #aimAt:with:", and we write this role (1, #aimAt:with:).
the role of the object dragon is: "to come second in the call to #aimAt:with:", which we write (2, #aimAt:with:).
the role of the object arrow is: "to come third in the call to #aimAt:with:", which we write (3, #aimAt:with:).
This has an important consequence: a method is generally not defined on a single object, but on a group of objects.
Let us take our example further and see how roles influence the way a method is selected by all objects in the tuple. For our method #aimAt:with:, we could have the following uses
archer aimAt: dragon with: arrow. |
hunter aimAt: deer with: rifle. |
kid aimAt: teacher with: bubbleGum. |
Although these examples use the same method signature, you would expect the effects and actions performed by the various objects taking part to the scene to vary: the arrow might hit the dragon without killing it and the archer have to flee from an angry beast; the deer might be killed and the hunter proudly show off in front of his friends; the bubble gum might miss its target and the kid be punished by his teacher. To further emphasize the fact that all objects took part to the action, the following examples would still lead to different consequences for the actors
archer aimAt: dragon with: rifle. |
hunter aimAt: deer with: bubbleGum. |
Let us go back to the statement: archer aimAt: dragon with: arrow. We will see in a moment how to control on which objects dispatch will happen. Let us suppose as a first step that the dispatch takes place only on the first object. The algorithm used to select the corresponding method resembles single dispatch: archer and its delegates are scanned until one that has a method for the role (1, #aimAt:with:) is found, or all delegates have been unsuccessfully scanned (remember that roles are stored on the objects themselves, just as slots are). The scan takes place on archer first, and then on each of its delegates, starting with the most recently added one. The scan then proceeds "depth first".
The following figure shows the order in which the objects are queried for the role. Each box represents an object and each arrow represents a delegation slot. For the needs of the picture, each object has an index; objects are queried in the order of their indexes.
If the role is found, then the corresponding method is executed. The objects dragon and arrow are passed as arguments to it, and the result, an object, is sent back to the caller (for example, the REPL, if that is where you evaluated the code from). The method executed is the "most specific" method that has the signature #aimAt:with: and applies to archer: if we call delegation distance the number of arcs in the graph of delegates for archer, then the method that is executed has the shortest delegation distance among all methods that could apply.
When scanning the delegates for matching methods, there can be some additional problems. A method may be found several times: in this case, it will be remembered only the first time it is found.
Consider for example the following diagram, where each arrow stands for a delegation slot and each box for an object. Suppose we order the object Eel to swim. The role (1, #swim) is stored on the object Fish. The lookup will find the method corresponding to the role (1, #swim) on Fish twice: once when considering FreshWaterFish delegates, and once when considering SeaFish delegates.
There may also be cycles in the delegation graph. They are detected and avoided. The following graph shows the delegation graph of the Slate object Traits traits (resulting from the execution of the unary method traits on the object Traits). Each arrow stands for a delegation slot, the name of the delegation slot being the same as the arrow label. A method defined on Traits traits and looked up from either Traits traits or Cloneable traits produces a cycle.
Let us now suppose in our example that dispatch happens on all objects. All objects participating in the call (archer, dragon and arrow) and their delegates are scanned independently for matching roles. The scan is the same on all objects, and the same as in the single dispatch case: it goes depth first, from the most recently added to the least recently added delegate. The goal of the search is to find methods that can be reached with each of the roles (1, #aimAt:with:), (2, #aimAt:with:), (3, #aimAt:with:), at the same time, in the respective delegation graphs of archer, dragon and arrow.
If at least one such method cannot be found, then a condition is signalled. Otherwise, we may be left with several methods to choose from. The choice is based on the delegation distance: each candidate method is found at some composite delegation distance (d1, d2, d3), where d1 is the delegation distance from archer, d2 the delegation distance from dragon and d3 the delegation distance from arrow. Let us order the methods lexicographically according to their (composite) delegation distances. The method that will be executed is the first according to that order. It is unique in the following sense: given the objects archer, dragon and arrow and the method signature #aimAt:with:, there cannot be a different method with the same delegation distance (d1, d2, d3).
This dispatch scheme has an important property that follows the intuition: the smaller the role of the object, the more specialized for that object the method is. In our case, the roles are, from the smaller to the grater: (1, #aimAt:with:) for archer, (2, #aimAt:with:) for dragon, (3, #aimAt:with:) for arrow. The method that will be executed is more specialized for archer, then for dragon, then for arrow, due to the lexicographic ordering used on delegation distances.
If the role (1, #aimAt:with:) is found on archer and on soldier, and archer delegates to soldier, then the method corresponding to the role on archer will be selected. In the same manner, if the role (2, #aimAt:with:) is found on dragon and beast, and dragon delegates to beast, then the method corresponding to the role on dragon will be given a preference.
When creating a method, you decide:
the role of each object, through the signature of the method
for which roles dispatch is going to happen
where to place the role (and reference the method) in the delegation graph for each object participating in the dispatch
![]() | Methods are not directly stored on objects. Instead, Slate uses maps from roles to methods, and to the Slate user, it looks as if each object just remembered its role for each method it can participate in. This is just the model for roles and methods; for efficiency, the implementation may differ. |
Single dispatch is just a special case of multiple dispatch, where you send the message on a tuple of objects but perform dispatch only on one of them. One important consequence of multiple dispatch is that the methods you need tend to be shorter than in a single dispatch only scheme, since you already have some knowledge about the objects themselves at the time of the call.