One thing that can make smalltalk code hard to read is the fact that almost everything is a message send, and that the receiver of a message send is always written first. For instance, in a send to
ifTrue:ifFalse: you always write the condition first because that is the receiver of the send:self size ~= other size ifTrue: [ ... ] ifFalse: [ ... ]When you read this line you have to read past the
self size ~= other size part before you see that what you're reading is actually the condition of a conditional. In this case the line is short so it's not a big problem but it does cost me just one or two extra brain cycles when reading the line. I would have saved those if you could write the conditional so that it was immediately clear, when reading from left to right, that it was a conditional. For instance:if: (self size ~= other size) ...The problem becomes much worse when you consider the operations whose receiver is a block, for instance some repetition, thread and exception operations, because blocks can be very long. An example is this code snippet taken from Squeak (well, slightly changed):
[What's happening here? The outer block is an exception handler, the body of which repeatedly performs some operation. When you read this code you have to read ahead several lines to understand what each block means. By the way, I have no idea what findThePig does.
[ | promise |
promise := Processor tallyCPUUsageFor: secs
every: msecs.
tally := promise value.
promise := nil.
self findThePig.
] repeat
] on: Exception do: [
^nil.
]
Another example, also taken from Squeak, is this:
[Here you not only have to read way ahead to see that the outer block forks a thread, but you have to look closely at the body of the inner block to spot the
[
newMorph := NetworkTerminalMorph connectTo:
self ipAddress.
WorldState
addDeferredUIMessage:
[newMorph openInStyle: #scaled] fixTemps.
]
on: Error
do: [ :ex |
WorldState addDeferredUIMessage: [
self inform: 'No connection to: ',
self ipAddress,' (',ex printString,')'
] fixTemps
].
] fork
on:do: selector which doesn't stand out at all but is very important in understanding the code. This is only two levels of nesting and if you add another level the code becomes even harder to read.Back when I wrote a lot of smalltalk I started inserting comments to make code such as this easier to read. For instance, I would insert "do" before a loop and "try" before an exception handler:
"try" [Here I don't have to read ahead because I can see which kind of construct I'm dealing with even before I see the opening bracket. You still have to read to the end to understand exactly how the code is repeated and which exceptions are caught, but my experience is that this gives just enough context that you can read and understand the code in one go, something I couldn't do with the previous version of the code, without the comments.
"do" [ | promise |
promise := Processor tallyCPUUsageFor: secs
every: msecs.
tally := promise value.
promise := nil.
self findThePig.
] repeat
] on: Exception do: [
^nil.
]
Back when we were discussing how to change the language, before we decided to go for a C/C++ style syntax, I suggested something called prefix keywords to make the language slightly more C-like and easier to read. It was never very popular but I still think it's a pretty decent idea. The suggestion was to allow keyword sends that start with a keyword. You can still write ordinary keyword sends such as:
(...) ifTrue: [...] ifFalse: [...]but you would also be allowed to write:
if: (...) then: [...] else: [...]which means sending the
if:true:false: message to the first expression, with the two blocks as arguments. You would declare the if:then:else: method on booleans like this (here on True) if: self then: thenPart else: elsePartThat way, instead of using comments to mark the beginning of constructs like above, you can use a prefix keyword:
^thenPart value
try: [There's no problem in parsing it and the semantics is as simple as the three other kinds of sends. It also works pretty well for methods such as
do: [ | promise |
promise := Processor tallyCPUUsageFor: secs
every: msecs.
tally := promise value.
promise := nil.
self findThePig.
].
] on: Exception do: [
^nil.
]
acquireDuring:. Considermutex acquireDuring: [compared with
... critical section ...
]
acquire: mutex during: [I think it's pretty neat. It does change the flavor of the code somewhat but I think it makes the code much easier to read.
... critical section ...
]

0 comments:
Post a Comment