Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Erasable nullables #24

Open
WhiteBlackGoose opened this issue Feb 13, 2022 · 4 comments
Open

Erasable nullables #24

WhiteBlackGoose opened this issue Feb 13, 2022 · 4 comments
Labels
Language idea One single idea which may be a start of a design document
Milestone

Comments

@WhiteBlackGoose
Copy link
Member

WhiteBlackGoose commented Feb 13, 2022

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?

@WhiteBlackGoose WhiteBlackGoose added the Language idea One single idea which may be a start of a design document label Feb 13, 2022
@LPeter1997
Copy link
Member

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
Copy link
Member

Kuinox commented Nov 25, 2022

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.

@WalkerCodeRanger
Copy link

I feel strongly that you should be allowed to have nested nullables (i.e. Null<Null<int>> or T??). My use case is for cases where you want to have a generic method return an optional value but allow the type parameter to also be nullable. For example, instead of the bool TryPop(...) that Stack<T> currently has, you could have a T? Pop() method. That would work well with ?? operator and with pattern matching.

@Kuinox
Copy link
Member

Kuinox commented Sep 22, 2024

This would necessitate that Null isn't a compile time only thing.
We will have union types so you could use an union types.
By the way I did not write it here, but to me ideally null are indicated through union types, like what you can do in typescript.

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
Language idea One single idea which may be a start of a design document
Projects
None yet
Development

No branches or pull requests

4 participants