Draco-lang / Language-suggestions

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

Erasable nullables #24

Open WhiteBlackGoose opened 2 years ago

WhiteBlackGoose commented 2 years ago

The key here is to keep C# interop both backward and forward (that is, use C# API from Fresh and use Fresh API from C#) but make sure to keep the code safe and consistent.

C#'s approach

C# has a separate type for value type nullables, but has annotations for reference types. It creates some inconsistent behaviour, to be precise:

Inconsistent deconstructure

object? a = ...
if (a is not null)
    a.Method(); // works

int? a = ...
if (a is not null)
    a.Method(); // doesn't work, Nullable<int> has no method that int has

Inconsistent generics

Consider two methods:

void Method1<T>(T? a);
void Method2<T>(T? a) where T : struct;

Let's substitute T = int:

void Method1(int a);
void Method2(Nullable<int> a);

See sharplab.

Erasable nullables

Universal type

Instead of having annotations and a type for value types, I suggest having a single type. To avoid confusion, let's call it Null for now:

[Erased]
type Null<T> = T | None

C# API from Fresh

C#:

void Method1(object a);
void Method2(object? a);
void Method3(int a);
void Method4(int? a);

Fresh:

void Method1(object a);
void Method2(Null<object> a);
void Method3(int a);
void Method4(Null<int> a);

Fresh API from C

It's exactly the same in the opposite direction. That's how it will be alised runtime-wise: present NRTs as Null<>, present Nullable<> as Null<>.

Null exists only at compile time

Null<string> s = GetString();
if (s is not null)
    return s.Split().Length;

Null<int> i = GetInt();
if (i is not null)
    return i;

Runtime-wise looks like

string s = GetString();
if (s != null)
    return s.Split().Length;

Nullable<int> i = GetInt();
if (i.HasValue)
    return i.Value;

Unresolved

As type argument

What about cases when there's no way to erase it? E. g. when used as a type argument:

void Method<T>(T a)
{
    SomeMethod<Null<T>>(a);
}

Or,

val list = List<Null<int>>(); // is it List<Nullable<int>> or List<Null<int>>
val list = List<Null<object>>(); // is it List<object?> or List<Null<object>>

Should we unconditionally materialize Null<> when used as a type argument?

Then typeof(Null<>) will work as expected.

Nested nullables

E. g.

Null<Null<int>> a = null;

Should all but the innermost materialize into Null? Should we prohibit this behaviour?

Relations with Options

Should we have explicit Option type if we manage to design a consistent nullable?

LPeter1997 commented 2 years ago

We have discussed this on the server, but just to have it documented here too.

If we can solve the compatibility issues, I'd be all for an explicit option type.

Kuinox commented 1 year ago

C# Nullables are metadata, attributes that doesn't offer any guarentee.
In theory, this is also true for the type system, but making these attribute lie is 'harder': casting is checked, and unsafe cast are 'hidden' behind Unsafe api.
On the other hand, it's very easy to make C# nullables lies, an operator ! exists just for this purpose. This is because C# nullables cannot cover every scenario.

On top of what @WhiteBlackGoose propose

I propose two interop behavior: Verify, and Trust.

Verify would be the default mode, it ensure every object annotated as non-nullable of a C# API by inserting runtime nullchecks when doing interop with C#. This would allow draco references to stay "safe" from badly annotated C# APIs.

Trust would be an opt-in mode. People seeking performance would opt-in this mode and no check would be performed, a badly annotated type reference will leak a null into draco code, may throw a nullref later, or pass this nullref back to C#, from a draco API declaring it give non null reference type.

Attributes should be provided to temporarly switch the behavior. I don't think trust is necessary, since badly annotated code can be considered as a bug, we could say we have nothing to do on our side. But since nullables attributes can easily lie, if Draco heavily rely on a lot of badly annotated C# code, Draco non-nullables types will look as unsafe and non reliables as C# ones.
The Verify mode allow the same level of strictness as the type system allow, but at runtime, since it's not possible to do better.