This week, I finished most of the inheritance checking for generics. However, while moving on to expression checking, I discovered a major syntax problem. Consider this:
<A, B> B foo(?A a) { // foo uses type parameters A and B, takes a ?A, and returns a B } // ... somewhere else: x = foo<uint, string>(7);
This is the syntax I was planning for calls to generic functions. The type arguments are specified after the function’s name and before the actual arguments, which makes for quite a readable syntax. The problem is that it can be parsed in two different ways:
x = a < b, c > (d + e);
Here, x
could be either a tuple of two booleans, containing the results of two comparisons, or the result of calling a function called a<b, c>
with the argument (d + e)
. Obviously, this syntax is not going to work.
Here are some of the ways that other languages specify generic types:
- C++ and C♯ use the syntax that I was going to use, but they don’t have native support for tuples, so they don’t have this problem.
- Java (which doesn’t natively support tuples either) forces the call to be qualified, and puts the type arguments before the name of the method:
this.<String, String>foo("hi");
I considered solving this problem by using the same syntax as Java. However, there is another complicating factor: function types. Function types can be generic, for example we can make bar()
return foo
:
{<A, B>: ?A -> B} bar() { return foo; } // now, we want to write the following, but how do we specify A and B? bar()(null);
Now, if we were using Java’s syntax, this would make it impossible to specify generic type arguments: doing <uint,string>bar()(null)
would be ambiguous: the type arguments could refer to bar or the function it returns.
The way that I’ve decided to handle this is to put the generic type arguments inside the parameter list:
foo(<uint, string>: null); bar()(<bigint, List<bigint>>: null);
This is slightly harder to read, but completely unambiguous, and works well with the syntax for the type of a generic function. Hopefully, plinth will be able to infer the type arguments in most cases, so that they do not need to be specified at all.
Creating Value Types
There is another problem with this syntax: it isn’t very well suited to creating value types (or compound types, as the syntax currently calls them). Ideally, when creating something, its type arguments should be specified the same way as they are in the type itself. For example:
Foo<uint, string> foo = new Foo<uint, string>();
But creating a value type is actually treated as a function call during parsing, because they look the same to the parser:
bigint b = bigint(4);
Unfortunately, with this new syntax for generics, constructor calls for value types will look nothing like the type being created:
foo<string, uint> a = foo(<string, uint>: ); // (no normal arguments, just type arguments)
As we have already discussed, just moving the type arguments outside the arguments list will not work. The only way to fix this is to use a similar syntax to a class creation: put a keyword at the start of the expression. This keyword should not be ‘new’, because ‘new’ implies that you aren’t creating a new one every time that you pass a value into a method. Instead, I have opted to use ‘create’, as it better illustrates the differences between class and value types:
string s = create string(new []uint {89, 69, 83}); foo<string, uint> a = create foo<string, uint>(s, s.length());
A nice side effect of this new keyword is that it also makes sense for constructors, and is shorter than the current ‘constructor’ keyword. So once this is implemented, constructors will be defined as follows:
class Foo { uint x; create(uint x) { this.x = x; } }
A similar change can be made for property constructors, which eliminates the need for ‘constructor’ to be a keyword at all.