Intercession without source changes
Methods in a Smalltalk object live in the method dictionary of its class. A method dictionary maps
Symbol
s to
CompiledMethod
s. From the virtual machine’s perspective, anything that understands
#run:with:in
is compatible with a
CompiledMethod
, in the sense that the VM sends this message to things that it will execute.
As a result, it’s easy enough to put arbitrary objects (that understand
#run:with:in
) in a class’s method dictionary. In fact, there’s a library for it.
With this hammer in hand, it becomes trivial to perform all manner of intercessions on code, without instrumenting code through a rewrite+compile cycle: wrap the
CompiledMethod
in an
ObjectAsMethodWrapper
(or several) and away you go. What might you do? Pre- and post-condition checking, flagging which methods execute for coverage analysis, profiling, and so on. Today we’re going to turn a method into a memoised one.
ObjectAsMethodWrapper subclass: #MemoizingWrapper
instanceVariableNames: 'usedArgs'
classVariableNames: ''
poolDictionaries: ''
category: 'ObjectAsMethodWrapper-Extra'
ObjectAsMethodWrapper >> initialize
super initialize.
usedArgs := Dictionary new.
ObjectAsMethodWrapper >> run: aSelector with: arguments in: aReceiver
| key |
key := {aReceiver}, aSelector, arguments.
usedArgs at: key ifPresent: [:value | ^ value].
^ usedArgs
at: key
put: (super run: aSelector with: arguments in: aReceiver)
With that in hand, let’s see how it behaves:
TestCase subclass: #MemoizingWrapperTest
instanceVariableNames: 'transcript'
classVariableNames: ''
poolDictionaries: ''
category: 'ObjectAsMethodWrapper-Extra-Tests'.
MemoizingWrapperTest >> setUp
transcript := OrderedCollection new.
MemoizingWrapperTest >> recordingMethod: anObject
transcript add: ('Executed #recordingMethod: with {1}' format: {anObject}).
^ anObject.
MemoizingWrapperTest >> testWrapperMemoizesCalls
| w |
"Wrap a single method."
w := MemoizingWrapper installOn: self class selector: #recordingMethod:.
["Sanity check"
self assert: transcript isEmpty.
"Wrapper leaves method result unchanged"
self assert: 1 equals: (self recordingMethod: 1).
"First execution, so of course the method's executed."
self assert: 1 equals: transcript size.
self recordingMethod: 1.
"Second execution with same argument returns memoized value
rather than executing the method again."
self assert: 1 equals: transcript size.
"New execution's result also unchanged."
self assert: 2 equals: (self recordingMethod: 2).
"And no further executions of the method have been logged."
self assert: 2 equals: transcript size.] ensure: [w uninstall]
This technique permits one to install arbitrary before/after/around method changing things around methods that you might not otherwise be able to change. It does, however, have a few drawbacks:
- Changes in behaviour are not reflected in changes in source.
- Like annotations, you can’t look at a method’s source and understand exactly how it will behave.
- Unlike annotations, without some kind of inspector you have no idea that a method is wrapped, or what it might do. At least it’s easy to find wrapped methods, if you thought to check for their presence:
ObjectAsMethodWrapper allInstanceswill let you iterate over all the wrappers.
