[hand with pencil]
Stuff For Sale
2004 Summer Tour
About
Blog
Class Stuff
Email Me
Events
Gallery
Home
In The Press
Newsletter
Services
Smalltalk
Veggie Van Gogh

Credits
© 2002,
Bytesmiths

[this is simply a banner and menu bar]

Please patronize sponsors of this page!

Bytesmiths no longer is involved in software consulting. Maintenance of this web site is currently subsidised by unrelated business activities. Please pass the word to other interested folks, so I can continue to host this page!
Your site could be listed here, for as little as $12 per month! Go to Bytesmiths Press for details.

This site has been selected by PC Webopaedia as one of the best on this topic!
This site has been awarded a Links2Go Key Resource Award in the Smalltalk category!

Originally published in The Smalltalk Report, May 1997.

It Depends on the Context

by Jan Steinman

"Everything is an object." Most Smalltalkers take this for granted, without really questioning all the implications, beyond wondering why 'Float pi * 2 / 2' is not the same object as 'Float pi'. It is easy to think of Collections as objects, and of course Points and Rectangles and such come naturally, and one can eventually get used to floating point numbers as objects.

Most Smalltalkers are aware that, unlike C++ or Java, Smalltalk classes are objects. Many also know that even methods -- the code you write in a browser -- are objects. But still, if everything is an object, then that means that some things that seem very non-object like must be hiding in Smalltalk somewhere.

What is a Context?

One of these strange objects is Context and its subclasses. If you remember back to the Dark Assembly Ages, you recall that when a subroutine is called via a JSR or CALL instruction, the information needed to make that subroutine work (arguments, temporaries, return address, etc.) is stored in a processor stack frame. The Smalltalk class Context is a model of such a stack frame for the Smalltalk virtual machine.

The following examples are based on the VisualWorks Context , which is fully general; it behaves like a real virtual machine stack frame in almost all circumstances. Also, it is easily accessed via the pseudo variable thisContext . It is also possible to access stack frame information in other Smalltalk dialects, but for the most part, they are not as general, and might not be useful for some of the techniques we demonstrate here. In particular, VisualAge does not have a real context object (although it allows limited access to contextual information), and Smalltalk/V derivatives can access (with limitations) the current method context with the expression '[] homeContext'.

What good are they?

The obvious use of Contexts (and only use, if you believe proponents of other languages) is in debugging. Since temporary variables are only found in Contexts , a debugger would be pretty useless without them. The arguments passed to methods are also available in coherent form only in Contexts . Sending messages to the Context is what allows you to single step in the debugger, as well as change temporaries and arguments.

However, the concept of "context as object" is much more powerful and useful than merely for debugging. Your code can access the run time state of your code for a reflective capability not found in most traditional languages. Of course, this can be abused, but it is especially useful when you are working with code that you do not control or do not wish to change.

Debugger return

Something that we cannot be without is a way to return "out of context" in the debugger. For instance, imagine that you have just stepped into an obviously defective method that you've been meaning to fix for some time, and want to simply get on through it, so you can discover the truly elusive bug beyond. "Damn! If only I could return 'true' here, so I can coax the sending method into its 'ifTrue:' block!" Well, you can! The following is for VisualWorks 2.5.1 with ENVY/Developer R3.01, but it should be easily adaptable to VisualWorks without ENVY.

First, we modify the debugger's contextMenu to add a 'return...' menu item, by adding:

  add: #return
    label: 'return...'
    enable: [self selectedMethod notNil]
    after: #proceed;
  yourself

at the end of the method. (A little further on we'll describe how you can change such menus once so that you can dynamically change them as often as you wish.) Now we need to define the return method:

EtDebugger
return
  "Return from the current context the
value of an expression entered by the user."
 
  | expr value ctx |
  context == nil ifTrue: [^self].
 
  context sender == nil ifTrue:[^self].

These first two statements ensure that a Context is selected in the list, and that there is at least one more Context below it to return to -- by definition, the bottom of the stack is the context whose sender is nil. Now we need to obtain an object to return to the sending Context :

  expr := Dialog
    request: 'Return the evaluation of this Smalltalk expression:'
    initialAnswer: 'self'.
    expr isEmpty ifTrue: [^self].
 
  value := context receiver class evaluatorClass new
    evaluate: expr
    in: context
    to: context receiver
    notifying: nil
    ifFail: [^Dialog notify: 'Bad expression entered.'].

The first statement above obtains an expression from the user, the second statement allows the user to abort by emptying the dialog.

The last statement evaluates the expression entered by the user, resolving it to an object. Because we can pass the current Context and the current receiver into the compiler (via the arguments for the keywords in: and to: ), the user can refer to instance variables and temporary variables in the method when she enters the expression.

  ctx := context sender push: value.
  [ctx willSend or: [ctx willReturn]] whileFalse: [ctx step].
  self resetContext: ctx.
  processHandle interrupted: true

Finally, these statements

  • push the new object to return onto the sender Context ,
  • optionally step the sender to a point where a return is possible,
  • reset the debugger's idea of what is happening, and
  • fake a "user interrupt" to the debugger, so it can resume the sender Context .

Now you can return any object you want from an arbitrary point in the debugger. (Try that with Java!)

isRecursive

A very legitimate use of Contexts is infinite recursion detection. What information is needed to answer the question, "Have I been in this code before, under identical circumstances?" "Well, that depends on the context!" might first be thought a glib answer, but it is literally true: the only legitimate, absolutely predictable way of detecting infinite recursion on an arbitrary method is through the Context .

Other means of recursion detection pale by comparison. For example, you can set an instance variable when you've been in a code segment, then check it before entering that segment. What happens if your code hits an exception after the "hasBeenHere" instance variable has been set, and before it gets cleared? Well, that code will never again be executed by that instance, since it now permanently thinks it's recursive.

Another popular recursion detection scheme is passing an argument around on the stack, which is nothing more than setting a flag in the Context . It tends to make things less readable, especially if the method could be unary or binary without the extra argument. Also, you can't do this unless you control all the methods in the recursion: if a single method sends itself, it works, but if the recursion includes other methods, you probably shouldn't be modifying them to pass your argument around!

So rather than change someone else's code, or set a flag somewhere that might not get unset, simply test "thisContext isRecursive" to detect infinite recursion:

Context
isRecursive
  "Do the sender's receiver, method, and
arguments appear previously in this context?
Will this context infinitely recurse?"
 
  | ctx rcvr meth |
  ctx := self.
  rcvr := self receiver.
  meth := self method.
  [ctx := ctx sender.
  ctx == nil] whileFalse:
    [ctx method == meth
      and: [ctx receiver == rcvr
      and: [(self home argumentsMatch: ctx)
      and: [^true]]]].
  ^false

This traverses the entire stack, looking for a Context where the method, receiver, and arguments are identical to the original Context . (The implementation of argumentsMatch: is left as an exercise for the reader!)

This does have a significant performance impact, but it can be tremendously useful as a temporary infinite recursion trap while debugging recursive code -- it's no fun to see the GC cursor start to flicker while madly mashing repeatedly on control C, hoping it will notice before you run out of memory!

senderPerform:withArguments:ifAbsent:

"Callbacks" have been around a long time, but recent advances in window systems have made them popular. Sometimes when a method is sent, it is really useful to talk to the object that initiated the request, generally in order to fill in some missing bit of information.

Typically, this requires passing an extra argument around, which is fine if it is a "tight" callback, with interaction between adjacent Contexts , but what about when you don't know how many stack levels separate your method's receiver from the one you need to query?

Context
senderPerform: selector withArguments: args ifAbsent: exception
  "Look for an object in the stack who can respond to selector .
Send that object the message selector with the arguments args .
If no respondents are found, execute exception ."
 
  self sendersDo: [:rcvr | (rcvr respondsTo: selector )
    ifTrue: [^rcvr perform: selector withArguments: args ]].
  exception value

This enumerates the context stack, looking for objects that can be sent selector , sending the message to the first one it finds. (Of course, finding an object that responds to selector is no guarantee that it responds the way you want it to!)

This uses a general purpose context stack enumeration method that can be handy in other situations:

Context
sendersDo: aBlock
  "Cause each object in the stack to execute aBlock
with itself as the argument."
 
  | ctx |
  ctx := self home.
  [aBlock value: ctx receiver.
  (ctx := ctx sender home) == nil] whileFalse: []

extendInSenderApplications

Our final example is a particular kind of "callback" that shows how useful thisContext can be when modifying base image code. A common need of tool builders is to add things to existing menus. However, that requires changing someone else's code, which increases the maintenance burden of the resulting code. As we mentioned in our column titled Managing Change (July, 1995), you must be extremely careful when changing code you don't own, whether it comes from your Smalltalk vendor, or from the guy in the next cubicle.

ENVY/Developer allows you to "extend" a class, by adding methods to the class while keeping those added methods in a different module than the module where the class is defined. This is an extremely useful way for toolsmiths to organize their additions. These modules are called "applications" in ENVY, and with a little help from the Context in which a menu is created, we can cause all ENVY applications that extend a class that defines a menu to have a chance at modifying that menu.

To do this, we added a "hook" method to Menu . Everywhere a menu is created, you must send this "hook" method as the last message to the menu. This means changing base image code in numerous places, but the overriding advantage is that you only make the change once, but afterward you can dynamically change the menu at will.

Menu
extendInSenderApplications
  "The sender of this method may have defined
new menu actions in an extension. Allow its
applications to modify me so that this new
behavior is user accessible. Since I am most
likely the last message in a cascade, answer
myself."
 
  self
    extendFor: thisContext sender receiver
    named: thisContext sender method selector

Since this is sent from the menu creation method, the sender is always the Context for a method defined by a browser to answer a menu, such as classesMenu , methodsMenu , or textMenu . The "sender receiver" is then the browser that uses the menu, and the "sender method selector" is the name of the method that answers the menu.

So now we have captured the contextual information needed to make a decision about how to modify the menu in order to use our browser additions. Next, we need to use that information to figure out which ENVY applications might have an interest in this menu:

Menu
extendFor: model named: modelSelector
  "Give each of model's applications, and each of model's superclass's
applications, an opportunity to modify me, and answer myself.
  modelSelector is simply passed along as an aspect. It is normally
the unary method selector sent to model to produce me, but it can be
any object. If I am a cached menu, I should only be sent this message
once, for obvious reasons!
  The purpose behind this is to allow a flexible mechanism for
extending the behavior of code that has fixed menus. If they send
this message, any of model's applications have an opportunity to
add to this menu. So an application that wants to add a menu item
to the ClassBrowser , for example, can intercept extendMenu:for:named:"
 
  (model class isMeta
    ifTrue: [model ]
    ifFalse: [model class]) withAllSuperclasses
      inject: IdentitySet new
      into: [:beenThereDoneThat :cls |
        cls applications do: [:app |
          "This check is not for performance. It is to avoid having
          the same things added twice."
          (beenThereDoneThat includes: app) ifFalse:
            [app extendMenu: self for: model named: modelSelector .
            beenThereDoneThat add: app]].
        beenThereDoneThat]

In this method, each ENVY application (always a SubApplication subclass) that defines or extends the browser that created this menu is sent extendMenu:for:named: , which in the simplest case, is a no op:

SubApplication class
extendMenu: menu for: model named: modelSelector
  "Allow me the opportunity to modify menu , which was probably
obtained from model by sending it modelSelector . However, all I
can assume is that I either define or extend model .
  The purpose behind this is to allow a flexible mechanism for
extending the behavior of code that has fixed menus. If the fixed
menu is sent extendInSenderApplications, any of model's
applications have an opportunity to modify that menu.
  For example, if AbstractMethodsBrowser>>informationMenu sends
extendInSenderApplications to the menu it creates, control
is passed (via this method) to any application that extends
AbstractMethodsBrowser . Typically, that extension would be
additional functionality in the form of a menu operation. This
method then provides a mechanism for that functionality to be
added to the informationMenu without changing the menu creation
method."

That isn't very exciting in the default case -- everything simply happens as it did before. However, by allowing dynamic changes to menus, you can make optimal use of ENVY's ability to load or unload applications.

For example, The Bytesmiths Toolkit™ has different modules that add different menu items to various browsers; our SmallDoc™ documentation system adds a load request notification, a code quality report, a release notes viewing facility, and a personal notes facility (among other things) to all menus that appear in lists of applications:

extendMenu: menu for: model named: modelSelector
  "Capture the various browser menus, and put SmallDoc menu
items on them."
 
  #applicationsMenu == modelSelector ifTrue:
    [menu
      addLine;
      add: #appLoadRequest
        label: 'load request...'
        enable: [model selectedApplications size > 0]
        for: model ;
      add: #codeReport
        label: 'code quality'
        enable: [model isOneApplicationSelected]
        for: model ;
      add: #viewReleaseNotes
        label: 'release notes'
        enable: [model isOneApplicationSelected]
        for: model ;
      addItemLabel: 'personal note' value: #editMyNote;
      ...] ifFalse:
    [#classesMenu == modelSelector ifTrue:
      [...] ifFalse:
    [...]]

while our BugTalker™ bug tracking system adds to those same application list menus a means of copying bug reports among application editions:

extendMenu: menu for: model named: modelSelector
  "If two editions of the same ENVY application are selected,
and the newer of the two is an edition, copy bugs forward from
the older one to the newer one."
 
  ...
  [(#applicationsMenu == modelSelector
    "If two editions of the same app are selected
    and: [(apps := model selectedApplications) size = 2
    and: [apps first name == apps last name
    and: [apps := apps asSortedCollection: [:a1 :a2 |
           a1 timeStamp <= a2 timeStamp].
    apps last isVersion not]]]) ifTrue:
      [menu
        addLine;
        add: [apps last propagateBugsFrom: apps first]
          label: 'copy bugs forward']]]

Thanks to thisContext , either or both of SmallDoc and BugTalker can be present, and their menu items will be dynamically added to the various system browsers. Unloading either or both modules causes their menu items to disappear.

What is the cost?

There is no free lunch. All these neat Context things have a cost. In particular, all modern, high performance implementations of Smalltalk cache context information within the virtual machine. If they did not, reasonable performance would be difficult, since Contexts are rapidly created and abandoned, and creating objects (and especially reclaiming their storage after they've been abandoned) is relatively expensive. When you refer to thisContext , you cause the internally cached context stack to instantiate real Context objects -- you defeat one of the major virtual machine optimizations!

As a guideline, referring to thisContext in code that must execute in the user interface time arena of high tens to low hundreds of milliseconds is probably not a bad idea, since it can simplify the code and ease maintenance and documentation needs. Our dynamic menu extension code is a good example -- the difference in performance is imperceptible.

On the other hand, using thisContext in code that must respond in the millisecond time arena is probably not a good thing to do, and sending a thisContext referencing method inside a millisecond time arena loop is probably deadly to performance!

Finally, "excessive contextitis" leads to write only code. Remember that most code is read many more times than it is written, and behave appropriately. Document your tricky Context hacks with in line comments, so the next person to stumble on them will have some chance of understanding what's happening, and may actually learn something. Be aware of the performance implications, and avoid "stealing the machine" simply because you want to play with your Context .

In summary, Contexts can be extremely useful in special purpose Smalltalk code, but they can be expensive in performance critical situations, and they can make code more difficult to understand if not used with care and properly documented.


Note 1: This is a performance-sensitive way to use the Context, since making a real Context object is not necessary in such cases. In most cases, passing contextual information as method arguments is preferable to referring to thisContext, although you can't do so many interesting things with it, and it is not as much fun!
Go to the previous column in the series, or the next column in the series.

160 Sharp Road, Salt Spring Island, British Columbia, V8K 2P6, Canada