Draco-lang / Language-suggestions

Collecting ideas for a new .NET language that could replace C#
75 stars 5 forks source link

[WIP] Language proposal 0.2 #40

Closed LPeter1997 closed 2 years ago

LPeter1997 commented 2 years ago

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:

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):

func foo[T](v: T): T = v;

func main() {
    foo[int](3);
}

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:

func foo[T, U](a: T, b: U): T = v;

func main() {
    foo[_, bool](3, true);
}

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: