Existent Types

JS++ invented the concept of "existent types" which enable developers to prevent out-of-bounds errors at compile time.

All Standard Library containers, such as System.Array and System.Dictionary will return existent types on container accesses.

Existent types use a + suffix. Arrays offer a way to learn about existent types:

1
2
int[] arr = [ 1, 2, 3 ];
int+ x = arr[0];

In the above code, the existent type int+ defines the range of possible values as the range of possible values for int (-2,147,483,648 to 2,147,483,647) plus the out-of-bounds value (undefined). JS++ differentiates from JavaScript in that, for internal types, undefined is only used for representing out-of-bounds accesses as all JS++ variables must be initialized and all functions must return.

Restrictions

Existent types cannot be used as the element type for arrays, and they cannot be used as a type argument to generic classes.

Thus, while the following is valid JavaScript, it is not valid JS++:

1
2
var arr1 = [ 1, 2, undefined, 3 ];
// int+[] arr1 = [ 1, 2, undefined, 3 ]; // Error because 'int+' is not a valid element type

The reason for this restriction is that the JS++ out-of-bounds value (undefined) cannot simultaneously also be a possible within-bounds value.

Operators

JS++ provides three operators for dealing with nullable types that also apply to existent types: ?? (safe default operator), ?. (safe navigation operator), and ?= (safe assignment operator).

Safe Default Operator

The safe default operator can be used for providing an alternative value if an out-of-bounds access was made:

1
2
3
4
5
import System;
 
int[] arr = [ 1, 2, 3 ];
int+ x = arr[Math.random(100)];
int y = x ?? 5; // Possible values for y: 1, 2, 3, 5
Safe Navigation Operator

The safe navigation operator can be used to access a class member or call a method without manually checking for an out-of-bounds access:

1
2
3
4
5
6
7
8
9
10
11
12
import System;
 
class Foo
{
    void doSomething() {
        Console.log("Something happened.");
    }
}
 
Foo[] arr = [ new Foo ];
Foo+ foo = arr[Math.random(100)];
foo?.doSomething();
Safe Assignment Operator

The safe assignment operator can be used to assign a different value if an out-of-bounds access occurred:

1
2
3
4
5
import System;
 
int[] arr = [ 1, 2, 3 ];
int+ x = arr[Math.random(100)];
x ?= 5; // Possible values for x: 1, 2, 3, 5

Combining with Nullable Types

Existent types can be combined with nullable types using the ?+ syntax:

1
2
3
4
5
6
7
8
9
10
11
import System;
 
Dictionary<bool?> inviteeDecisions = {
    "Roger": true,
    "Anton": true,
    "James": null, // James is undecided
    "Qin": false
};
 
bool?+ isJamesAttending = inviteeDecisions["James"]; // 'null'
bool?+ isBryceAttending = inviteeDecisions["Bryce"]; // 'undefined'

API Re-design

Existent types introduce new best practices. Consider the following case:

1
2
3
4
5
6
7
8
9
10
import System;
 
class Foo {}
 
void log(Foo foo) {
    Console.log(foo.toString());
}
 
Foo[] arr = [ new Foo() ];
log(arr[0]); // ERROR

In JS++, "non-nullable" classes don't have a default value. While we can resolve the above error by providing a new instance of Foo as a default value, this is not always desirable:

1
2
3
4
5
6
7
8
9
10
import System;
 
class Foo {}
 
void log(Foo foo) {
    Console.log(foo.toString());
}
 
Foo[] arr = [ new Foo() ];
log(arr[0] ?? new Foo); // Fixes the error, but what if we don't want a new instance?

Instead, with existent types, a new approach is necessary. Due to the language design, most JS++ code is naturally modular and object-oriented. Free functions and static methods are not easy to test either. By changing our free function that accepts Foo to an instance method of Foo, we can use existent types more freely and correctly:

1
2
3
4
5
6
7
8
9
10
11
import System;
 
class Foo
{
    void log() {
        Console.log(this.toString());
    }
}
 
Foo[] arr = [ new Foo() ];
arr[0]?.log(); // Use the ?. operator instead of ??, which requires us to provide a default value

Casting

Sometimes, we know we are accessing an in-bounds element. In such cases, we can cast:

1
2
3
4
class Foo {}
 
Foo[] arr = [ new Foo() ];
Foo foo = (Foo) arr[0];

In other cases, we can't infer from the source code whether or not the accessed element is in-bounds. However, we know we have checked the value so a cast is still safe:

1
2
3
4
5
6
7
8
9
10
11
import System;
 
class Foo {}
 
Foo[] arr = [ new Foo() ];
int element = Math.random(1, 100);
if (arr[element] != undefined) {
    Foo foo = (Foo) arr[element];
 
    // ...
}

Unlike casting from other types, casting from nullable or existent types will only strip the nullable or existent type. It will not simultaneously allow downcasting. If you desire to downcast, you need to downcast separately.

Casting can throw a CastException. As a result, it is preferred to avoid casting when using existent types and prefer methods such as API re-design instead.

Guidelines

Never use existent types where nullable types will suffice. In the JS++ Standard Library, existent types are used solely for representing whether a container element exists and is within bounds. It is strongly advised against using existent types outside of container APIs or data structures.

See Also

Share

HTML | BBCode | Direct Link