Smalltalk: doesNotUnderstand:

Isotropic Selectors

June 16, 2004 17:03:16.651

Made any Rectangles lately? Is it left:right:top:bottom:? or is it top:bottom:left:right? or top:left:bottom:right:? LayoutFrames are even funner. Any time you get these long multiple arguments message, you have candidates for these keyword lists where the order isn't really important.

I've only played with Lisp a little, but I remember one of the cool things you could do was list the keywords in any order you wanted. Something like (Point y: 1 x: 4) (it's been a while so forgive me if that's not exactly right). What would it take to have the same ease and flexibility in Smalltalk?

doesNotUnderstand: comes to the rescue one again! Anytime you mess with dnu:, want to build (and test) the facilities first, saving the hacking of dnu: till the end. There's a couple methods that come into play here, we'll go one by 1.

Unique Keywords

We need a way of determining whether a method signature is even a candidate for keyword rearrangement. To be so, each of its keywords must be unique. We can keywords to a symbol to get an Array of the different keywords. We could just send keywords asSet size = keywords size, but that seemed computationaly expensive, so I implemented:

SequenceableCollection>>hasUniqueElements
1 to: self size - 1
		do: 
			[:n | 
			| candidate |
			candidate := self at: n.
			n + 1 to: self size do: [:m | (self at: m) = candidate ifTrue: [^false]]].
	^true

It's about 15x faster than the one liner.

Unique Keywords

The next part is being able to determine if two selectors are "isotropic". I think I can use that term, if not, I'm sure someone will correct me. Basically we want to be able to detect that x:y: and y:x: are the same selectors, but with different keyword ordering. We can extend any class, we want, so how about Symbol.

Symbol>>isotropicMatch: aSymbol 
	"aSymbol is assumed to be multi and and keyword unique"

	| mine his |
	self size = aSymbol size ifFalse: [^false].
	self last = $: ifFalse: [^false].
	mine := self keywords.
	his := aSymbol keywords.
	SequenceableCollectionSorter sort: mine.
	SequenceableCollectionSorter sort: his.
	^mine = his

We want this one to go fast too. We're going to be scanning ALL of a given receivers selectors, so we want it to resolve quickly. The first two lines of the method are quick and get rid of almost all match candidates. We only have to run the last 5 lines on rare cases, almost always on a match.

Having this code, we can make sure that #x:y: isotropicMatch; #y:x: returns true, and #x:a: isotropicMatch: #x:y: does not.

Hack it in

This could probably be factored better. You have to do some of the work in the existing Object>>doesNotUnderstand: method though, because you want to return from it. One of the cool things I noticed doing this is that you can override a method in <None> When you're done, you just unload <None> and everything goes back to normal. Basically, we want to extend the default doesNotUnderstand: message so that it first checks to see if it's just an order mangled version of something it could understand, and if so, do so. Bolting this code

	aMessage arguments size > 1 
		ifTrue: 
			[(self findIsotropicSelector: aMessage selector) ifNotNil: 
					[:isoSelector | 
					| argMap |
					argMap := Dictionary new.
					aMessage selector keywords with: aMessage arguments
						do: [:a :b | argMap at: a put: b].
					^self perform: isoSelector
						withArguments: (isoSelector keywords collect: [:each | argMap at: each])]].

to the front of Object>>doesNotUnderstand: and add this supporting method:

findIsotropicSelector: aSelector 
	aSelector keywords hasUniqueElements ifFalse: [^nil].
	^self class allSelectors detect: [:each | each isotropicMatch: aSelector]
		ifNone: [nil]

and it should work. The helper method enumerates the receiver's selectors looking for an isotropic match. The preamble to dnu: uses it to to determine if there's an isotropic equivalent, and if so, reorders the arguments with a map, and resends.

The Result

This is kind of fun. (Point y: 3 x: 5) = (5 @ 3) returns true! Of course, Rectangle top: 4 bottom: 6 left: 1 right: 3 prints as "1@4 corner: 3@6". You don't have to stick to class side instance creation methods either. You can do (Array new: 1) put: 'hi' at: 1! I don't know, I think that's kind of cool.

Comments

[anthony] June 16, 2004 20:09:45.822

Here's a challenge: Can you implement multi-methods using doesNotUnderstand: ?

Re: Isotropic Selectors

[David Buck] June 17, 2004 7:01:31.358

Comment on Isotropic Selectors by David Buck

Does the loss of reflection ability, higher difficulty of maintenance, more difficult debugging, tricky code, and higher complexity concern you at all?

Maybe I've spent too much of my Smalltalk career in quantum leap mode - "leaping from project to project setting right what was once done wrong and hoping each time that my next leap will be the leap home."

Re: Isotropic Selectors

[ Reinout Heeck] June 17, 2004 7:52:50.620

Comment on Isotropic Selectors by Reinout Heeck

Dave, look at it like this ;-)

Learning by playing

[David Buck] June 17, 2004 9:23:15.277

I have absolutely no problems with playing around with Smalltalk to learn it better and learn how far you can push it. In fact, I find Travis' articles on DNU quite thought provoking. Why DO we need to order keywords anyways? I can't think of any cases where unique keywords shouldn't be able to be rearranged.

My problem is when I see things like this popping into production code and people have to debug it. I like having senders and implementers work. I like the fact that when I rename a method in the RB, that it's guaranteed to rename all senders. This trick breaks that guarantee.

As a learning exercise, I applaud Travis for his creativity and for pushing the limits of the language. I just don't want to deal with tricks like this in code I have to maintain.

OTOH

[Isaac Gouy] June 17, 2004 13:11:35.485

Seems like I've spent enough time ripping out 'cool' code and replacing it with boring vanilla alternatives, to appreciate David's viewpoint. OTOH method wrappers and dnu can be really useful.

Even some Java like languages provide named parameters (and optional parameters, and multiple dispatch), so how cool is this?

void main(String[] args){
   let p1 = new Point(x: 2, y: 3);
   let p2 = new Point(y: 3, x: 2);
   println (p1.x == p2.x && p1.y == p2.y);
}

class Point { int x; int y; }
Nice

Re: Isotropic Selectors

[ Travis Griggs] June 17, 2004 14:22:30.299

Comment on Isotropic Selectors by Travis Griggs

Isaac, I guess it was cool inside of the traditional Smalltalk context. But I agree with you; it's nothing novel at all. I was inspired by Lisp's inspiration, and as you point out, many other languages use that approach for function argument ordering. Heck the day before, we did the varargs thing which is directly inspired by good old C's ..... argument type. :)

Maybe what was cool/fun about it, was that I could sit down and in less time than it took me to blog about it, implement it. I didn't have to write a new language.

Language

[Isaac Gouy] June 17, 2004 14:50:02.247

"didn't have to write a new language"
Really? Didn't we just change the syntax?

(It is cool that the language implementation is mostly written in Smalltalk - that makes it easy for anyone to create new languages based on Smalltalk.)

Re: Isotropic Selectors

[ Travis Griggs] June 17, 2004 15:46:16.761

Comment on Isotropic Selectors by Travis Griggs

"Really? Didn't we just change the syntax?"

I guess it depends on what you mean by syntax. I guess I think of syntax traditionaly as what the compiler deals with. I did not change the compiler one iota. I used A) the fact that Smalltalk is late bound AND provides a hook for dealing with binding misses and B) the fact that even its message sending and binding is reified as objects into the environment itself.

interesting

[keith ray] June 21, 2004 22:17:06.956

This sounds like a good proof of concept, but the idea might be better implemented in the Compiler or the browser/editor. For example, in the compiler, make the 'actual' symbol for a method be a sorted list of the method's 'keyword' names ( #bottom:left:right:top: ) and have the compiler find that method no matter what order the calling code uses them... that work could be done interactively in the browser as well -- a little like the function-name-completion capabilities in various modern (Java/C/C++/C#) IDEs.