The goal of this document is to expand on the ideas laid out in the 0.1 proposal, to slowly work towards a complete language. Programs in the 0.1 proposal could already do simple calculations. While this proposal won't extend the capabilities much, it's working towards a language that is able to build up abstractions with user-defined types and generics.
Scope of the features
The primary topic of this proposal will be the type system:
Generics
Overloading
Type inference
User defined record types
For loops
Just like last time, the proposal implicitly defines the initial syntax.
Generic functions
Generic type parameters can be introduced after the function name between [...]. For example:
func second[T, U](x: T, y: U): U = y;
The rationale for leaving <> is that they are binary operators, which can really complicate the compiler in very undesirable ways - see what C++ goes through while parsing, or what Rust introduces syntactically not to make it painful. The simplest is just to take an operator that already comes in pairs, and [] is already used by languages like Scala.
Function overloading
While functions were already proposed in 0.1, function overloading was unspecified. I see no reason to disallow it, overloading should stay. A concrete function signature should bind stronger than a generic one. For example:
func foo[T](v: T): T = v; // (1)
func foo(v: int): int = v; // (2)
func main() {
var a = foo(true); // (1) is called
var b = foo(1); // (2) is called
}
This should be simple enough, but once subtyping comes into play, the rules might be complicated, we should keep that in mind. With the current rules we can already produce ambiguous calls - meaning it's not too hard to overcomplicate this system:
func foo[T](x: T, y: int) : int = 0; // (1)
func foo[T](x: bool, y: T): int = 0; // (2)
func main() {
var a = foo(true, 1); // Both (1) and (2) match the 'same amount'
}
Type inference
One of the main strengths of the language should be a way stronger type-inference than what C# allows. A good example on a permissive - but not ML-level - type-inference system can be found in Rust: signatures must be fully typed, but inference can work freely in the function-local scope.
Return type inference
Functions with inline expressions should allow return-type inference:
func f1(x: int) = x; // OK, inferred to be (int) -> int
func f2() = Console.WriteLine(""); // OK, inferred to be () -> unit
func f3() { // ERROR: functions with a body need explicit return type,
// assumed to be unit otherwise
return 1;
}
Variable type inference
Variables can be declared without type, even when they do not get assigned a value immediately:
var x: int = 4; // OK, explicitly typed, value matches
var y = 4; // OK, inferred immediately to be int from the value
var z: int; // OK, explicitly typed
var w;
w = 1; // OK, inferred to be int from usage
var q; // ERROR: Could not infer type of the variable
Generic type argument inference
When a generic function matches the best for a function call, the generic types would be inferred, no need to specify call arguments, just like in C#:
func foo[T](v: T): T = v;
func main() {
foo(3); // T = int
}
The generic arguments can be explicitly specified too, in case it can not be inferred (or for explicitness):
The _ essentially means to create a type variable that will be inferred by the compiler. It can be used in any context, just like any other type.
Incomplete inference
An incomplete inference will result in an error. For example:
var q; // Without any usage of q
Any type is considered incomplete, that contains type variables.
User-defined record types
See #41 for the design documentation.
For loops
I believe a single for-each should be fine, if we can ease the range-creation a bit. Something like:
for (i in range(0, 10)) {
WriteLine("Hello, " + i);
}
The type could be specified after the variable name, like for (i: int in range(0, 10)) .... The variable would be a val, meaning it can't be reassigned.
Under the hood, it would be desugared into a while-loop, similarly to C#:
for (i in range(0, 10)) {
WriteLine("Hello, " + i);
}
// Becomes
var enumerator = range(0, 10).GetEnumerator();
while (enumerator.MoveNext()) {
val i = enumerator.Current;
WriteLine("Hello, " + i);
}
Future ideas
Ideas that came up while writing this proposal:
named arguments for function calls
named explicit generic arguments
optional arguments (maybe even with non-constant expressions?)
Goal of the document
The goal of this document is to expand on the ideas laid out in the 0.1 proposal, to slowly work towards a complete language. Programs in the 0.1 proposal could already do simple calculations. While this proposal won't extend the capabilities much, it's working towards a language that is able to build up abstractions with user-defined types and generics.
Scope of the features
The primary topic of this proposal will be the type system:
Just like last time, the proposal implicitly defines the initial syntax.
Generic functions
Generic type parameters can be introduced after the function name between
[...]
. For example:The rationale for leaving
<>
is that they are binary operators, which can really complicate the compiler in very undesirable ways - see what C++ goes through while parsing, or what Rust introduces syntactically not to make it painful. The simplest is just to take an operator that already comes in pairs, and[]
is already used by languages like Scala.Function overloading
While functions were already proposed in 0.1, function overloading was unspecified. I see no reason to disallow it, overloading should stay. A concrete function signature should bind stronger than a generic one. For example:
This should be simple enough, but once subtyping comes into play, the rules might be complicated, we should keep that in mind. With the current rules we can already produce ambiguous calls - meaning it's not too hard to overcomplicate this system:
Type inference
One of the main strengths of the language should be a way stronger type-inference than what C# allows. A good example on a permissive - but not ML-level - type-inference system can be found in Rust: signatures must be fully typed, but inference can work freely in the function-local scope.
Return type inference
Functions with inline expressions should allow return-type inference:
Variable type inference
Variables can be declared without type, even when they do not get assigned a value immediately:
Generic type argument inference
When a generic function matches the best for a function call, the generic types would be inferred, no need to specify call arguments, just like in C#:
The generic arguments can be explicitly specified too, in case it can not be inferred (or for explicitness):
Type placeholder
The
_
can be used as a placeholder type, which can be useful when working with generics, only wanting to specify some of the type arguments:The
_
essentially means to create a type variable that will be inferred by the compiler. It can be used in any context, just like any other type.Incomplete inference
An incomplete inference will result in an error. For example:
Any type is considered incomplete, that contains type variables.
User-defined record types
See #41 for the design documentation.
For loops
I believe a single for-each should be fine, if we can ease the range-creation a bit. Something like:
The type could be specified after the variable name, like
for (i: int in range(0, 10)) ...
. The variable would be aval
, meaning it can't be reassigned.Under the hood, it would be desugared into a while-loop, similarly to C#:
Future ideas
Ideas that came up while writing this proposal: