Switching in Smalltalk
In my article on Smalltalk a while ago, I talked about creating a custom switch statement. I thought I’d talk a bit more about this, how to make it, how to customize it, and other solutions to this problem.
Intended Methods
First, I should go over the most common ways to do this, which you should be using most of the time.
Polymorphism
First, the easiest case is when the condition you’re testing for is the class of some object. In that case, just give the common superclass a method, and implement that method once in each subclass. Use the different implementations for different cases. For example, this is how booleans are implemented:
True >> ifTrue: trueBlock ifFalse: falseBlock [
^trueBlock value
]
False >> ifTrue: trueBlock ifFalse: falseBlock [
^falseBlock value
]
This method only works in the case that your condition can be expressed in the form “what class is object x”, but if you can do it that way you should! This method:
- is easy to program
- is easy to extend, even by third parties
- puts each case in its own method, avoiding long methods
Switch Methods
A more versatile way to do switches is with switch methods. In essence, you just need to write a bunch of if-a-return-b statements, like this:
colour = #red ifTrue: [ ^'The colour is red.' ].
colour = #blue ifTrue: [ ^'The colour is blue.' ].
colour = #green ifTrue: [ ^'The colour is green.' ].
^'The colour is something else.' "else clause"
This method:
- works for any kind of switch conditions
- allows you to control the order the conditions are evaluated in
- is, for better or worse, difficult to extend
- requires you to return from the function at the end of each case, preventing things like nested switch statements
My Custom Method
None of the above methods worked for one of my use cases, so I had to make a custom, fully featured switch statement. The statements themselves look like this:
{
(colour = #red) -> 'The colour is red.'.
(colour = #blue) -> 'The colour is blue.'.
(colour = #green) -> 'The colour is green.'.
(self hasYellowMessage & (colour = #yellow)) -> 'The colour is yellow.'.
true -> [ self error: 'Unrecognized colour. ]. "else clause"
} switch
The parts on the left of the -> symbols are the conditions, and the parts on the right are the cases. This statement goes through each condition one by one, yielding the case corresponding to the first condition that evaluates to true. Both conditions and cases can be blocks; only the case block of the first true condition is executed.
In the following sections, I’ll show you the source code for the switch message, and how you can customize it.
Basic
This is the most basic variant of the code, which acts as above, and returns nil if no condition matches.
Collection >> switch [
^self detect: [ :each | each key value ] ifFound: [ :each | each value value ] ifNone: nil.
]
Error on Exhaust
By changing the contents of the ifNone: part, you can cause the switch to raise an error if no condition matches, or give it any other behaviour.
Collection >> switch [
^self detect: [ :each | each key value ] ifFound: [ :each | each value value ] ifNone: [ self error ].
]
Switch on Object
This variant switches on an object, evaluating the block corresponding to its value:
Object >> switch: aCollection else: elseBlock [
^aCollection detect: [ :each | each key value = self ] ifFound: [ :each | each value value ] ifNone: elseBlock.
]
The syntax looks like this:
3 switch: {
1 -> [ self thisCaseIsNotExecuted ].
2 -> [ self thisCaseIsNotExecuted ].
3 -> [ self thisCaseIsExecuted ].
4 -> [ self thisCaseIsNotExecuted ].
} else: [ self error: 'Not in the range 1-4' ]
Collection Cases
Finally, this variant of the object switch allows you to use collections instead of individual objects. If the part to the left of the -> is a collection, the case is executed if any of its members matches.
Object >> switch: aCollection else: elseBlock [
^aCollection
detect: [ :each | | keys |
keys := each key value.
keys = self or: [ keys isCollection and: [ keys includes: self ] ]
] ifFound: [ :each | each value value ] ifNone: elseBlock.
]
An example of one of these statements:
3 switch: {
(1 to: 2) -> 'Very small'.
(3 to: 8) -> 'Moderate'. "this case is executed"
(9 to: 99) -> 'Kind of high'.
(100 to: 9999) -> '3 or 4 digits'.
} else: [ self error: 'Unrecognized number' ]
Combining this with polymorphism
This method can also be used to improve the polymorphism method. Its problem is that it only works if your switch is already over the type of an object. Instead, we can add a switch statement into the class to determine what subclass to use.
This method does mean that users don’t have control over the order the cases execute in, so this is best for switches that select a different case for each value of a certain object. Here’s the code for that, where #conditions is a shared variable on Superclass.
Superclass class >> initialize [
conditions := Dictionary new.
self subclassesDo: [ :each | each putConditions: self ].
]
Superclass class >> at: aValue putCase: anObject [
conditions
at: aValue
ifPresent: [ self error: 'Subclass attempted to add a duplicate rule.' ]
ifAbsentPut: [ anObject ]
]
Superclass class >> switch: aValue [
conditions ifNil: [ self initialize ].
conditions
at: aValue
ifPresent: [ :caseObject | caseObject case ]
ifAbsent: [ self error: 'No class found for this case.' ].
]
Superclass class >> putConditions: aClass [ self subclassResponsibility ]
Superclass >> case [ self subclassResponsibility ]
SubclassOne class >> putConditions: aClass [
aClass at: #firstValue putCase: self new
]
SubclassOne >> case [
Transcript show: 'First case'; cr
]
SubclassTwo class >> putConditions: aClass [
aClass at: #secondValue putCase: self new
]
SubclassTwo >> case [
Transcript show: 'Second case'; cr
]
Then, the code Superclass switch: x will print ‘First case’ if x is #firstValue, and ‘Second case’ if x is #secondValue.
The advantages of this are:
- No matter how many cases you add, you won’t have any long methods (except if your individual cases are long).
- The switch can easily be extended, even by other packages, by just adding more subclasses.
- Subclass can be given properties to allow them to contain multiple cases. You just need to have the putConditions: message send at:putCase: multiple times, one for each case.
Some things to note:
- The cases are being run in separate methods, so any variables they access need to be passed in as arguments, and/or returned from case. Every case method needs to have the same arguments.
Other Links
Here are some other links about switching that might be useful:
- Another Case for Smalltalk: A more powerful switch statement by Dr. Dave Mason
- An old Pharo forum discussion about why they removed something similar to my switch statement