|
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!
- Bytesmiths Editions -- large, archival, fine-art photography on unusual materials
- Bytesmiths Press -- artists' services: web design/hosting, jury slides, giclee reproductions, opening announcements, brochures, etc.
- Champagne Beadworks -- handcrafted jewelry and beadwork
- Crafted By Carol -- handcrafted jewelry and beadwork
- Dharm Atma Yoga -- Kundalini yoga instruction
- EcoReality, an organization devoted to establishing a sustainable ecovillage
- Ecovillage Newsletter -- Diana Leafe Christian's news of her travels.
- Environmental Education Outreach -- providing environmental education worldwide.
- Gemini Gypsy -- Carole Good-Hanson's fused glass frames
- Green Chipper -- light forestry and environmental services.
- Salt Spring Island Society for Community Education -- community education on our island of 10,000.
- Veggie Van Gogh -- two artists' mobile warehouse and living quarters, petroleum-free!
- Veggiemog -- life and times of Kelly O'Toole's Unimog, running on biodiesel
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.
|