stz multiple returns?
Multiple return values. Why not? After all we've already thrown out the idea of exceptions so may be we should be looking at other ways to handle errors. One way is to have 'in' and 'out' variables which is something we could totally do. In the C world you often return the error code an modify the guts of one of your parameters which will be a pointer.
With multiple return values, however, you can have your callee create something and return it and also an error code if necessary. Now. I get the desire to have tagged unions instead and do a switch like statement to determine what the type really is and stick with single return value. I think we should explore these ideas a little more.
First off we need a scenario. The scenario will be finding a person with the name Jane. The code for that should look pretty straight forward. Let's give it a go.
people find: [ each: person → ? | ^each name == 'Jane' ]
Although let's let the type inferencer do some of the lifting for us. One of our goals was to figure out how much typing would be necessary to still have performant code:
people find: [ each | ^each name == 'Jane' ]
The question is what does find return?
Scenario 1: multiple return values:
jane, ok := people find: [ each | ^each name == 'Jane' ]
!ok then: [ jane := make-person: 'Jane' ]
Scenario 2: tagged unions:
maybe_jane := people find: [ each | ^each name == 'Jane' ]
maybe_jane == not-found then: [ maybe_jane := make-person: 'Jane' ]
Scenario 3: the Smalltalk way:
jane := people find: [ each | ^each name == 'Jane' ] else: [ ^make-person: 'Jane' ]
(Requiring the ^ everywhere is starting to become a bore. May be that was a bad idea...)
Are there any performance costs to consider with the three approaches? Considering each version has to in some way test if there was a jane then the answer is basically no. Complexity, perhaps, in terms of implementation if you're looking at tagged unions.
The smalltalk way wins here. It's clear and concise and if we remove the ^'s which seem to be noise we're left with:
jane := people find: [ each | each name == 'Jane' ] else: [ make-person: 'Jane' ]
Let's make it slightly more interesting and have an error code returned too. The two error codes will be not-found and access-denied. I don't know why there's be access-denied given that by its very nature it leaks information that there is a Jane, you're just not allowed to get at it... but anyway, contrived examples are contrived.
jane := people find: [ each | each name == 'Jane' ]
else: [ error |
error == not-found then: [ ^make-person: 'Jane' ]
error == access-denied then: [ panic: error ]
panic: error // unhandled error type ]
Now back to scenario 1. Let's see if the Smalltalk way is still the nicest:j
jane, status := people find: [ each | each name == 'Jane' ]
status == found then: [ do-jane-stuff: jane ]
status == not-found then: [ do-jane-stuff: (make-person: 'Jane') ]
status == access-denied then: [ ^nil, access-denied ]
panic: status // unhandled error type
Less indenting. More explicit. Let's try again with Scenario 2, tagged unions:
jane := people find: [ each | each name == 'Jane' ]
jane is: person then: [ do-jane-stuff: jane ]
jane == not-found then: [ do-jane-stuff: (make-person: 'Jane') ]
jane == access-denied then: [ ^access-denied ]
panic: jane // unhandled error type
The tagged unions win here. It's the same code as multiple returns but one less variable. Speaking of switch statements - may be we should invent one. Okay let's give that a try:
jane is: person then: [ do-jane-stuff: jane ],
== not-found then: [ do-jane-stuff: (make-person: 'Jane') ],
== access-denied then: [ ^access-denied ],
[ panic: jane ] // unhandled error type
Does this work as a concept for switches? This is similar to the cascade syntax of Smalltalk which uses the ; instead of the ,. The , here represents a list if things, always. It would be up to the compiler to recognise that this is an opportunity to optimise. After all the type of jane is going to be tagged-union of: person, find-error.
We might have to revisit this soon. Either way it seems that tagged unions have won this round. Multiple return values are a poor cousin and Smalltalk-like flow control makes it hard to follow what's going on. As powerful as they are (and the language would support you doing it that way) they aren't necessarily the best core library choice.
As an added bonus we can look at composing aggregate code blocks when the input is one thing and the output is one thing.
I'm going to go out on a limb and say that may be the language should support multiple return values - just not for this kind of scenario. After all consider the classic swap:
tmp := a
a := b
b := tmp
It's so much nicer to be able to write it like this:
a, b := b, a