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:

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:

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:

Some things to note:

Here are some other links about switching that might be useful: