Search code examples
smalltalksqueak

How can I add methods to a class at runtime in Smalltalk?


I'm building a Smalltalk API to an XML-based web service. The XML service is so regular that, rather than write the methods by hand, I figured I'd just override #doesNotUnderstand: to dynamically add methods via MyApi class>>compile:, then call all the methods once in a workspace, then remove the DNU and have my nice API.

This works great, but passing a giant string to #compile: just feels really wrong to me; in Python and other languages, I'd be able to attach a nicely syntax-checked lambda to a class to achieve a similar effect in a safer manner. E.g.:

def himaker(name):
    def hello(self, times):
        for x in xrange(times):
            print "Hi, %s!" % name
    return hello
class C(object): pass
C.bob = himaker('Bob')
C.jerry = himaker('Jerry')
a = C()
a.bob(5)

versus

SomeObject>>addHello: name
    | source methodName |
    methodName := 'sayHello', name, 'Times:'.
    source := String streamContents: [ :s |
         s nextPutAll: methodName, ' count'.
         s nextPut: Character cr.
         s nextPut: Character tab.
         s nextPutAll: 'count timesRepeat: [ Transcript show: ''Hi, ', name, '!'' ].' ]
    SomeObject class compile: source

Surely there must be something as clean as the Python version?


Solution

  • If you just want the source string to more clearly reflect the method:

    SomeObject>>addHello: name
      | methodTemplate methodSource |
      methodTemplate := 'sayHello{1}Times: count
      count timesRepeat: [ Transcript show: ''Hi, {1}!'' ].'.   
      methodSource := methodTemplate format: { name }.
      self class compile: methodSource.
    

    If you want the source to be syntax-checked, you could start with a template method like this:

    sayHelloTemplate: count
        count timesRepeat: [ Transcript show: 'Hi, NAME' ].
    

    And then fill the template accordingly, like:

    addHello2: name
        | methodTemplate methodSource |
        methodTemplate := (self class compiledMethodAt: #sayHelloTemplate:) decompileWithTemps.
        methodTemplate selector: ('sayHello', name, 'Times:') asSymbol.
        methodSource := methodTemplate sourceText copyReplaceAll: 'NAME' with: name.
        self class compile: methodSource.
    

    Of course, all of this would be clearer if some methods were extracted :)