I've been thinking about the problem of passing a Sequence of values to a sequenced parameter in Ceylon (a varargs
parameter in Java terminology). Consider:
void print(Object... objects) { ... }
String[] words = {"hello", "world"}; print(words); //what does this do?
Does the second line mean that we're passing a single Sequence<String> to print(), or two Strings? Java behaves very strangely in this situation:
//Java:
print(new String[]{"hello", "world"}); //passes two Strings with a compiler warning asking for an explicit cast to Object[]
print(new Object[]{"hello", "world"}); //passes two Objects with no compiler warning
print(new String[]{"hello", "world"}, new String[]{"hello", "world"}); //passes two String[] arrays as varargs!
Ugh!
Things gets even a little more complicated when you have a generic method like this:
T? first<T>(T... objects) { ... }
String[] words = {"hello", "world"}; first(words); //what type should be inferred for T?
This really starts to screw up my beautiful clean type argument inference algorithm! Which is why this issue is coming up now - it's a corner case that I only noticed once I actually implemented generic type argument inference in the type analyzer.
So I think we need to make you explicitly specify what you mean when you pass a sequence of values to a sequenced parameter. I have a couple of ideas about how to do this.
Solution 1
First solution, kinda indirectly inspired by groovy, would be a special syntax in the positional parameter method invocation protocol.
String[] words = {"hello", "world"}; print(words); //pass a single String[] print(words...); //pass two Strings String[]? words2 = first(words); //infers T = String[]; String? word = first(words...); //infers T = String
I think this reads fairly naturally. The downside is it's a special-purpose kind of punctuation that needs to be specially explained in the specification.
Solution 2
Second solution is to introduce a special type (a subtype of Sequence) to represent a package of sequenced arguments. Call it SequencedArguments. Then, with a little helper method spread() that wraps up a Sequence as a SequencedArguments, the syntax would look like:
String[] words = {"hello", "world"}; print(words); //compiler automatically produces a SequencedArguments<String[]> print(spread(words))); //explicitly pass a SequencedArguments<String> String[]? words2 = first(spread(words)); //infers T = String[]; String? word = first(spread(words)); //infers T = String
This is a little more verbose, but reasonable. It also makes the specification easier to write.
Solution 3
Solutions 1 and 2 can be combined very elegantly. We can define:
- T... means SequencedArguments<T> for any type T
- e... means SequencedArguments(e) for any expression e
So T... is just a type name abbreviation like T[] and T?, and e... is just an operator expression. We end up with exactly the same syntax as Solution 1, but with the semantics of Solution 2.
I think this works out, and is very much in the spirit of the language. On the other hand, if T... is just an ordinary type declaration, I don't know how we can go about enforcing that a sequenced parameter must be the last parameter in a parameter list. I kinda like the fact that this is an error:
void print(Object... objects, OutputStream stream) { ... } //compile error?
WDYT? Does print(words...) read well to you guys, or does it feel arbitrary?
P.S.
Let's not confuse this too much with the idea of applying an operation to a tuple of arguments like what you can do in functional languages and dynamic languages. This is superficially similar, but not quite the same.
UPDATE
A reasonable syntactic variation that perhaps reads somewhat better would use all as a keyword:
void print(Object all objects) { ... }
print(words); //pass a single String[] print(all words); //pass two Strings
I could probably get into this if I didn't just hate the idea of keywordizing the very useful word all
.