Belte in its current state is largely similar to C#, but many ambitious diverging features are planned for the (near-ish) future. This document goes over some of the planned features as well as smaller features needed before taking them on. This includes features present in C# that aren’t in Belte yet.
C#-style tuples that represent a small collection of values. This includes implicit-typing and deconstruction:
(int, bool) MyFunc() {
return (10, true);
}
var (myInt, myBool) = MyFunc();
For-each loops would also support deconstruction:
(int, string)[] myArr = /* ... */;
for ((iInt, iString), idx in myArr) {
// ...
}
Multiple levels of conciseness for different circumstances. Lambdas would create closures.
// The 4 following lambdas are functionally equivalent
int(int) squareFunc = x => x * x;
int(int) squareFunc = (x) => x * x;
int(int) squareFunc = int(int x) => x * x;
int(int) squareFunc = int(int x) => { return x * x; };
int s = squareFunc(10); // s = 100
The more verbose variants are used in situations where the types cannot be inferred:
var squareFunc = x => x * x; // What is the type of x?
var squareFunc = int(int x) => x * x; // Gives enough information to deduce the type of squareFunc as `int(int)`
Interfaces make generics much more useful. These would be C#-style interfaces with the only notable difference being a non-combined base-list for classes (Java-style):
C#
class A : BaseClass, IInterface1, IInterface2 { }
Belte
class A extends BaseClass implements Interface1, Interface2 { }
Unreal 6 comes with a new scripting language called “Verse”. I believe its approach to concurrency/async code execution is much cleaner that C#’s design.
syncThis relies on tuples. A sync block is a list of operations that run concurrently (this means shared
state must be thread-safe or locked, and the order in which operations finish is nondeterministic). In its most verbose
form, each nested block indicates a concurrent operation:
var (result1, result2) = sync {
{
return SomeOperation1();
}
{
return SomeOperation2();
}
}
Where the return value of SomeOperation1 is placed in result1 and the return value of SomeOperation2 is placed in
result2. Both operations are run in parallel. Any statements inside of a nested block run linearly:
var (result1, result2) = sync {
{
SomeCall1();
SomeCall2();
return SomeOperation1();
}
{
return SomeOperation2();
}
}
Instead of blocks, an expression can be used:
var (result1, result2) = sync {
SomeOperation1();
SomeOperation2();
}
async and suspendAn operation in a sync block can be any ordinary code, but async code is also supported. A block marked async
supports suspending that task in a non-blocking way.
To sleep a task for a certain amount of time, a suspend statement can be used which sleeps a specified number of
seconds:
var (result1, result2) = sync {
async {
SomeCall1();
suspend 3.5; // suspends this task for 3.5 seconds
return SomeOperation1();
}
SomeOperation2();
}
A method can also be marked async, in which case an await expression can be used:
var (result1, result2) = sync {
await SomeAsyncOperation1();
await SomeAsyncOperation2();
}
raceThe race block executes a list of cancellable methods and returns the result of the first one that succeeds
(non-except):
var result = race {
SomeMethod1();
SomeMethod2();
SomeMethod3();
}
The other methods are cancelled once one of them succeeds. This is done by hidden checking of cancellation tokens at
certain points. These points are inserted by the compiler on any method marked cancellable hence the requirement of
each race method call being a cancellable method. The user can insert explicit cancellation checks with
checkpoint.
cancellable int MyMethod() {
for (var i = 0; i < 10000; i++) {
checkpoint; // cancellation checked here
//...
}
// ...
}
cancelTo forcibly make an async method cancel and fail in a race, a cancel statement can be used.
var result = race {
SomeMethod1();
SomeMethod2();
SomeMethod3();
}
cancellable int SomeMethod1() {
if (!TrySomeCall())
cancel;
return SomeCall2();
}
branchThe branch block runs code in the background while continuing execution immediately past the block. The block is
awaited at scope exit:
int MyMethod() {
branch {
SomeCall1();
SomeCall2();
SomeCall3();
}
// Work continues immediately, the three calls above run in the background linearly
var result = SomeOperation();
return result; // branch code is awaited here
}
The branch task cannot be cancelled. To do that, a spawn expression should be used instead.
spawnThe spawn expression creates and starts a task. It can be managed or let free (which should only be done in rare
circumstances). A captured task must be a cancellable method. Tasks support .Cancel() and .Await().
var task = spawn SomeCancellableOperation();
task.Cancel(); // uses `cancellable` hidden cancellation checks
When a task is let free, it can contain any call because it can’t be cancelled. It always won’t be awaited at scope exit:
void MyMethod() {
spawn free SomeNonCancellableOperation();
} // task keeps running even after `MyMethod` returns
lockMany structures are not concurrency-safe. In such a case, a lock can be used to ensure only the task with the lock is
reading or writing the captured value:
lock (myList) {
myList.Append(10);
}
A Language Server Protocol would massively boost ergonomics by offering intellisense to code editors. This requires a semantic model which is a large undertaking. This change is mostly behind the scenes, but know its coming (eventually)!
An alternative to interfaces. Instead of using virtual dispatch at runtime, has constraints would allow static
duck-typing at compile-time which improves performance. The main complication is integrating this with the .NET runtime,
which is currently unsolved.
void M<type T>(T param) where { T has void SomeMethod(int); } {
param.SomeMethod(10);
}