New issue

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

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nullable Reference Types #317

Open
wants to merge 44 commits into
base: master
from

Conversation

Projects
None yet
5 participants
@cartermp
Member

cartermp commented Jun 27, 2018

This is a draft of F# nullable reference types.

It is based primarily on the C# proposal, with additional considerations for F# based on existing behavior we have.

I expect a lot of things that are written down to change over time (i.e., a good lot of what I've written may not actually work out well, but we'll see 馃槃 )

@cartermp cartermp changed the title from [DRAFT] Nullable Reference Types to Nullable Reference Types Jun 29, 2018

Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
## Alternatives
[alternatives]: #alternatives
The primary alternative is to simply not do this.

This comment has been minimized.

@dsyme

dsyme Jul 17, 2018

Contributor

I think there are lots of other alternatives we should list. e.g. foremost is this:

  1. Design a fully sound (apart from runtime casts/reflection, akin to F# units of measure) and fully checked non-nullness system for F# (which gives errors rather than warnings), then interoperate with C# (giving warnings around the edges and for interop)

Given that F# is in good shape w.r.t. nullness this is actually not such a silly thing to consider.

Another is

  1. Allow code to be generic w.r.t. nullness (i.e. so you can express "if you give me a null in, I might give you a null back; if you give me a non-null in, I will give you a non-null back")
@dsyme

dsyme Jul 17, 2018

Contributor

I think there are lots of other alternatives we should list. e.g. foremost is this:

  1. Design a fully sound (apart from runtime casts/reflection, akin to F# units of measure) and fully checked non-nullness system for F# (which gives errors rather than warnings), then interoperate with C# (giving warnings around the edges and for interop)

Given that F# is in good shape w.r.t. nullness this is actually not such a silly thing to consider.

Another is

  1. Allow code to be generic w.r.t. nullness (i.e. so you can express "if you give me a null in, I might give you a null back; if you give me a non-null in, I will give you a non-null back")

This comment has been minimized.

@cartermp

cartermp Jul 31, 2018

Member

Added

@cartermp
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
Show outdated Hide outdated RFCs/FS-1060-nullable-reference-types.md
@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Jul 17, 2018

Contributor

I left a lot of comments, but basically I feel this is roughly in the right zone.

Contributor

dsyme commented Jul 17, 2018

I left a lot of comments, but basically I feel this is roughly in the right zone.

@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Jul 17, 2018

Contributor

Some offline discussion with @cartermp:

  • We discussed [<DefaultValue>] attributes on fields. "Yes, you should add a TBD for that in the RFC, also for CLIMutable, default struct values and Unchecked.defaultof<_> if they aren't there already."

  • Re DefaultValue - I think I'd be OK with a warning for this case. The DefaultValue thing is in practice it's not where I wanted it to be. The original intent was roughly that DefaultValue(check=false) would be like Unchecked.defaultof<_> - an "ugly unchecked" construct. And plain DefaultValue would be checked and nice. But bugs like fsharp/fslang-suggestions#484 mean that the checking is imperfect and in practice DefaultValue is a source of unchecked nulls.

  • Separately, I suppose the LINQ methods like x.FirstOrDefault() will have return type T | null? I need to consider what that means for the type system, e.g. if T is instantiated to int32, does that mean there is some rule during substitution that int32 | null --> int32. Odd.... That's not exactly what I think of when I see T | null. It's almost like it's T | default in that case...

  • I did wonder if maybe F# -> F# violations of nullability should be errors, akin to Units of Measure. i.e. if you declare let f (x : string | null) : string = x then you get an error. With some zero-runtime-cost construct to explicitly cast your way out of this.

Contributor

dsyme commented Jul 17, 2018

Some offline discussion with @cartermp:

  • We discussed [<DefaultValue>] attributes on fields. "Yes, you should add a TBD for that in the RFC, also for CLIMutable, default struct values and Unchecked.defaultof<_> if they aren't there already."

  • Re DefaultValue - I think I'd be OK with a warning for this case. The DefaultValue thing is in practice it's not where I wanted it to be. The original intent was roughly that DefaultValue(check=false) would be like Unchecked.defaultof<_> - an "ugly unchecked" construct. And plain DefaultValue would be checked and nice. But bugs like fsharp/fslang-suggestions#484 mean that the checking is imperfect and in practice DefaultValue is a source of unchecked nulls.

  • Separately, I suppose the LINQ methods like x.FirstOrDefault() will have return type T | null? I need to consider what that means for the type system, e.g. if T is instantiated to int32, does that mean there is some rule during substitution that int32 | null --> int32. Odd.... That's not exactly what I think of when I see T | null. It's almost like it's T | default in that case...

  • I did wonder if maybe F# -> F# violations of nullability should be errors, akin to Units of Measure. i.e. if you declare let f (x : string | null) : string = x then you get an error. With some zero-runtime-cost construct to explicitly cast your way out of this.

@cartermp

This comment has been minimized.

Show comment
Hide comment
@cartermp

cartermp Jul 31, 2018

Member

@dsyme Addressed all feedback as best as I can. The most notable changes are removing constraint syntax and changing the non-nullability assertion mechanism from a postfix operater to a function, with the postfix operator listed as an alternative below.

Member

cartermp commented Jul 31, 2018

@dsyme Addressed all feedback as best as I can. The most notable changes are removing constraint syntax and changing the non-nullability assertion mechanism from a postfix operater to a function, with the postfix operator listed as an alternative below.

Conceptually, reference types can be thought of as having two forms:
* Normal reference types
* Nullable reference types

This comment has been minimized.

@jcouv

jcouv Jul 31, 2018

The C# spec will also have the concept of "oblivious" (although the term may change). It means string outside of a NonNullTypes context, such as a C# 7.0 library. Never mind, it looks like F# won't have such contexts.

@jcouv

jcouv Jul 31, 2018

The C# spec will also have the concept of "oblivious" (although the term may change). It means string outside of a NonNullTypes context, such as a C# 7.0 library. Never mind, it looks like F# won't have such contexts.

#### Null assignment and passing
A warning is given if `null` is assigned to a non-null value or passed as a parameter where a non-null reference type is expected:

This comment has been minimized.

@jcouv

jcouv Jul 31, 2018

Do you mean: ... is assigned to a non-null value variable ...?

@jcouv

jcouv Jul 31, 2018

Do you mean: ... is assigned to a non-null value variable ...?

This comment has been minimized.

@jcouv

jcouv Jul 31, 2018

Also, aside from (1) assignment, and (2) argument-passing, there probably also is third case for returning. Or is that part of the "assignment" category?

@jcouv

jcouv Jul 31, 2018

Also, aside from (1) assignment, and (2) argument-passing, there probably also is third case for returning. Or is that part of the "assignment" category?

This comment has been minimized.

@cartermp

cartermp Jul 31, 2018

Member

Re: value -> variable - we use value to refer to many things in F#, distinguishing between a value and a mutable value (with immutable the default).

Re: return value - I've added another example in the code sample that shows that if you attempt to assume the result of a function that returns a nullable reference type to a value that is explicitly non-nullable, it's a warning.

@cartermp

cartermp Jul 31, 2018

Member

Re: value -> variable - we use value to refer to many things in F#, distinguishing between a value and a mutable value (with immutable the default).

Re: return value - I've added another example in the code sample that shows that if you attempt to assume the result of a function that returns a nullable reference type to a value that is explicitly non-nullable, it's a warning.

```fsharp
// Inferred signature:
//
// val makeNullIfEmpty : str:string -> string | null

This comment has been minimized.

@jcouv

jcouv Jul 31, 2018

let neverNull (str: string) =
    match str with
    | "" -> ""
    | _ -> makeNullIfEmpty(str)

How do I prevent a string | null inference (when user knows better than compiler)?
Is there some ! operator as in C#?

Maybe a helper method would to the trick... ('T | null) -> 'T

@jcouv

jcouv Jul 31, 2018

let neverNull (str: string) =
    match str with
    | "" -> ""
    | _ -> makeNullIfEmpty(str)

How do I prevent a string | null inference (when user knows better than compiler)?
Is there some ! operator as in C#?

Maybe a helper method would to the trick... ('T | null) -> 'T

This comment has been minimized.

@cartermp

cartermp Jul 31, 2018

Member

Yep, this would require non-nullability assertions. I'll make a note of that in this section.

To prevent this, you'd need to do something like this:

let neverNull (str: string) =
    match str with
    | "" -> ""
    | _ -> 
        makeNullIfEmpty str
         |> Unchecked.notNull

Where Unchecked.notNull (or whatever the mechanism is, perhaps !) will assert non-nullability.

@cartermp

cartermp Jul 31, 2018

Member

Yep, this would require non-nullability assertions. I'll make a note of that in this section.

To prevent this, you'd need to do something like this:

let neverNull (str: string) =
    match str with
    | "" -> ""
    | _ -> 
        makeNullIfEmpty str
         |> Unchecked.notNull

Where Unchecked.notNull (or whatever the mechanism is, perhaps !) will assert non-nullability.

@dsyme

This comment has been minimized.

Show comment
Hide comment
@dsyme

dsyme Jul 31, 2018

Contributor

Notes from a discussion with @TIHan on possible implementation approaches, and also some discussion about FSHarp.Core.

General implementation approach:

Null checking is different to byref checking because AFAICS it also involves inference, and in particular inference that changes ("enhances" I suppose) the types reported across assembly boundaries.
For example, the code

let s = [ "a"; null ]

presumably infers type string? and the code

let s = [ "a" ]

presumably infers type string. This information needs to be propagated both through the checking of the whole file, the assembly and across assembly boundaries

The way we currently manage this is in type checking, so I don't think we can do it as a separate phase. Well, we could perhaps do it as a separate phase but we'd have to mutate a lot of the objects we've already created, and propagate the results around.

TAST TType adjustment?

So I think the way to approach it would be to put nullness info (and variables) into the TAST for types. This is, however, quite invasive on the codebase. For example we might start with this:

| TType_app of TyconRef * Nullness * TypeInst
...
type Nullness = { mutable Info : NullnessInfo }
type NullnessInfo = | Unknown | Null | NotNull

One basic problem is that TType_app is used in, like, 202 places or more in the code. Not the end of the world but a lot.

Another problem is the pickled info format in TastPickle.fs. We have to keep that format stable (keep reading old formats, don't produce incompatible new formats for DLLs that aren't compiled with the new switch) but still encode the information. We'll find some way to do that I suppose.

Filling in the NullnessInfo on import from .NET isn't that hard I suppose: if it's coming from a new-switch .NET DLL then fill in NotNull by default etc.

With all that in place, I suppose we could start to implement rules that actually interpret this information, e.g. in ConstraintSolver.fs and TypeRelations.fs. Basically if NotNull meets Null then emit a warning, and if Unknown meets either then mutate it to whichever. This has to be done with Undo in ConstraintSolver.fs, and there will be some other subtleties.

So you want to extend each case of TType with nullness possibly? Meaning, not only TType_app, but others as well? That's a spec question that comes down to

  • Can we have type (int * int) | null, i.e. can reference tuples be nullable? I suppose so.
  • Can we have type (int -> int) | null, i.e. can function types be nullable? I suppose so
  • Can we have type T | null. I've been pushing back strongly on the C# notion that a type variable T is by default non-nullable - that makes little sense to me as a default

Separately, I'll mention that there is another alternative to

| TType_app of TyconRef * TypeInst * Nullness 
| TType_tuple of TupInfo * TTypes * Nullness 

which is to instead use a single new case

| TType_nullness of TType * Nullness

That may well work out more cleanly but we'd have to establish exactly where these can occur and carefully look for recursive descent cases where nullness is relevant.

FSharp.Core and F# metadata format

There is this also a problem about FSharp.Core - will it "use the new features" or not? That's really a major issue. If it doesnt, it won't be consumable by downlevel compilers. If it doesn't, does that effect things badly? e.g. will strings returned by String.replicate be assumed to be nullable? That would be so wrong.... Or will we have to manually annotate the whole API of FSharp.Core (I guess we will)

We can treat FSharp.Core as a separate design point. There are several meanings of "use the new features" and we need to clarify that. And there are several ground rules, such as FSharp.Core remains binary compatible, and preferably continues to have F# metadata consumable by downlevel compilers (part of what it means to be binary compatible in my eyes).

Yea, I agree with that. Basically, existing code will always have same metadata and will work with older compilers. If you try to use a new feature, well it's undefined.

I think it will be more subtle than that. FSharp.Core may well be compiled with the feature enabled and get a null-checked interpretation when consumed by a new compiler. But it must get the existing interpretation when consumed by an old compiler. Exactly how we do that is TBD.

I see because if we annotate a bunch of stuff in FSharp.Core, it can change the metadata.

yes

And older compilers need to work with that

Yes, whatever it means to "annotate the metadata" can't intrude with the functioning of an old compiler.

Do we have to store it in metadata? I assume an attribute is enough info to know

At least, that's my position for now.

Do we need to store it in metadata? Well, it feels like we do. I mean this is type information and flows across assembly boundaries. But yes, simply emitting identical metadata for FSharp.Core would be ok, and perhaps with an associated extra metadata bloc with the nullness values. It's hard to rely on attributes as the code to correlate F# TAST stuff to .NET stuff would be really gnarly.

I see. So anything that is defined in F#, we store it in metadata rather than relying on attributes? We emit attributes, but only for someone on the outside to consume? Is that right?

Yes. F#-to-F# is always mediated via the F#-specific metadata today

Contributor

dsyme commented Jul 31, 2018

Notes from a discussion with @TIHan on possible implementation approaches, and also some discussion about FSHarp.Core.

General implementation approach:

Null checking is different to byref checking because AFAICS it also involves inference, and in particular inference that changes ("enhances" I suppose) the types reported across assembly boundaries.
For example, the code

let s = [ "a"; null ]

presumably infers type string? and the code

let s = [ "a" ]

presumably infers type string. This information needs to be propagated both through the checking of the whole file, the assembly and across assembly boundaries

The way we currently manage this is in type checking, so I don't think we can do it as a separate phase. Well, we could perhaps do it as a separate phase but we'd have to mutate a lot of the objects we've already created, and propagate the results around.

TAST TType adjustment?

So I think the way to approach it would be to put nullness info (and variables) into the TAST for types. This is, however, quite invasive on the codebase. For example we might start with this:

| TType_app of TyconRef * Nullness * TypeInst
...
type Nullness = { mutable Info : NullnessInfo }
type NullnessInfo = | Unknown | Null | NotNull

One basic problem is that TType_app is used in, like, 202 places or more in the code. Not the end of the world but a lot.

Another problem is the pickled info format in TastPickle.fs. We have to keep that format stable (keep reading old formats, don't produce incompatible new formats for DLLs that aren't compiled with the new switch) but still encode the information. We'll find some way to do that I suppose.

Filling in the NullnessInfo on import from .NET isn't that hard I suppose: if it's coming from a new-switch .NET DLL then fill in NotNull by default etc.

With all that in place, I suppose we could start to implement rules that actually interpret this information, e.g. in ConstraintSolver.fs and TypeRelations.fs. Basically if NotNull meets Null then emit a warning, and if Unknown meets either then mutate it to whichever. This has to be done with Undo in ConstraintSolver.fs, and there will be some other subtleties.

So you want to extend each case of TType with nullness possibly? Meaning, not only TType_app, but others as well? That's a spec question that comes down to

  • Can we have type (int * int) | null, i.e. can reference tuples be nullable? I suppose so.
  • Can we have type (int -> int) | null, i.e. can function types be nullable? I suppose so
  • Can we have type T | null. I've been pushing back strongly on the C# notion that a type variable T is by default non-nullable - that makes little sense to me as a default

Separately, I'll mention that there is another alternative to

| TType_app of TyconRef * TypeInst * Nullness 
| TType_tuple of TupInfo * TTypes * Nullness 

which is to instead use a single new case

| TType_nullness of TType * Nullness

That may well work out more cleanly but we'd have to establish exactly where these can occur and carefully look for recursive descent cases where nullness is relevant.

FSharp.Core and F# metadata format

There is this also a problem about FSharp.Core - will it "use the new features" or not? That's really a major issue. If it doesnt, it won't be consumable by downlevel compilers. If it doesn't, does that effect things badly? e.g. will strings returned by String.replicate be assumed to be nullable? That would be so wrong.... Or will we have to manually annotate the whole API of FSharp.Core (I guess we will)

We can treat FSharp.Core as a separate design point. There are several meanings of "use the new features" and we need to clarify that. And there are several ground rules, such as FSharp.Core remains binary compatible, and preferably continues to have F# metadata consumable by downlevel compilers (part of what it means to be binary compatible in my eyes).

Yea, I agree with that. Basically, existing code will always have same metadata and will work with older compilers. If you try to use a new feature, well it's undefined.

I think it will be more subtle than that. FSharp.Core may well be compiled with the feature enabled and get a null-checked interpretation when consumed by a new compiler. But it must get the existing interpretation when consumed by an old compiler. Exactly how we do that is TBD.

I see because if we annotate a bunch of stuff in FSharp.Core, it can change the metadata.

yes

And older compilers need to work with that

Yes, whatever it means to "annotate the metadata" can't intrude with the functioning of an old compiler.

Do we have to store it in metadata? I assume an attribute is enough info to know

At least, that's my position for now.

Do we need to store it in metadata? Well, it feels like we do. I mean this is type information and flows across assembly boundaries. But yes, simply emitting identical metadata for FSharp.Core would be ok, and perhaps with an associated extra metadata bloc with the nullness values. It's hard to rely on attributes as the code to correlate F# TAST stuff to .NET stuff would be really gnarly.

I see. So anything that is defined in F#, we store it in metadata rather than relying on attributes? We emit attributes, but only for someone on the outside to consume? Is that right?

Yes. F#-to-F# is always mediated via the F#-specific metadata today

@cartermp

This comment has been minimized.

Show comment
Hide comment
@cartermp

cartermp Aug 1, 2018

Member

@dsyme

I think it will be more subtle than that. FSharp.Core may well be compiled with the feature enabled and get a null-checked interpretation when consumed by a new compiler. But it must get the existing interpretation when consumed by an old compiler. Exactly how we do that is TBD.

Perhaps I'm missing something, but isn't this how the annotations will work? That is, if a compiler does not understand what these are, it should just ignore them and see reference types as the same reference types it knows.

We'd need to verify this of course, but I would think that selective annotation accomplishes what we want.

Member

cartermp commented Aug 1, 2018

@dsyme

I think it will be more subtle than that. FSharp.Core may well be compiled with the feature enabled and get a null-checked interpretation when consumed by a new compiler. But it must get the existing interpretation when consumed by an old compiler. Exactly how we do that is TBD.

Perhaps I'm missing something, but isn't this how the annotations will work? That is, if a compiler does not understand what these are, it should just ignore them and see reference types as the same reference types it knows.

We'd need to verify this of course, but I would think that selective annotation accomplishes what we want.

@cartermp

This comment has been minimized.

Show comment
Hide comment
@cartermp

cartermp Aug 6, 2018

Member

Notes from meeting with C# team:

  • Names for attributes are not yet set in stone (but changes will be recorded in their spec).
  • Null obliviousness is the default for non-F# reference types today. Changing this to be nullable may be too invasive for existing code, even though the warnings are valuable.
  • NonNullTypes(true|false) is not easily controlled with project options or the project file. May want to reconsider statements about configuration given this.
  • "Intent warnings" are being used for C#, which we should consider for things like [<DefaultValue>] and [<CLIMutable>]
  • Changing options to warn on Some null in source and emit FSharpOption<'T> where T is treated as non-null makes the type closer to what is intended for non-F# consumers. Could consider this.
  • Consider not complaining against redundant null checks. For example, checking for null as a library author even when types are declared as non-null makes you resilient to poor use of a library.
  • If null obliviousness is done, how does that play into tools? C# is considering some additional notation here, though they aren't quite sure how to do it yet. But they will need to show tooltips differently for a value if it is oblivious and then treated as either nullable or non-nullable later.
  • Null obliviousness is highly related to "we don't know if this is nullable or not", where the latter is what we'll need to represent in the compiler to properly handle type inference. If null obliviousness is done for F#, is it an additional flavor of reference type? If so, is that flavor a compiler implementation detail, or is it surfaced into signatures? If it's surfaced, how to represent with F# signature files? Should it even be representable?
  • Is an IDE action to turn a warning into an error in an ad-hoc way? Not sure yet.
  • C# is considering a "meta code" to represent the whole group of nullability/non-nullability warnings so that it can be warnaserror'd as a group. F# may wish to consider this as well.
Member

cartermp commented Aug 6, 2018

Notes from meeting with C# team:

  • Names for attributes are not yet set in stone (but changes will be recorded in their spec).
  • Null obliviousness is the default for non-F# reference types today. Changing this to be nullable may be too invasive for existing code, even though the warnings are valuable.
  • NonNullTypes(true|false) is not easily controlled with project options or the project file. May want to reconsider statements about configuration given this.
  • "Intent warnings" are being used for C#, which we should consider for things like [<DefaultValue>] and [<CLIMutable>]
  • Changing options to warn on Some null in source and emit FSharpOption<'T> where T is treated as non-null makes the type closer to what is intended for non-F# consumers. Could consider this.
  • Consider not complaining against redundant null checks. For example, checking for null as a library author even when types are declared as non-null makes you resilient to poor use of a library.
  • If null obliviousness is done, how does that play into tools? C# is considering some additional notation here, though they aren't quite sure how to do it yet. But they will need to show tooltips differently for a value if it is oblivious and then treated as either nullable or non-nullable later.
  • Null obliviousness is highly related to "we don't know if this is nullable or not", where the latter is what we'll need to represent in the compiler to properly handle type inference. If null obliviousness is done for F#, is it an additional flavor of reference type? If so, is that flavor a compiler implementation detail, or is it surfaced into signatures? If it's surfaced, how to represent with F# signature files? Should it even be representable?
  • Is an IDE action to turn a warning into an error in an ad-hoc way? Not sure yet.
  • C# is considering a "meta code" to represent the whole group of nullability/non-nullability warnings so that it can be warnaserror'd as a group. F# may wish to consider this as well.
@cartermp

This comment has been minimized.

Show comment
Hide comment
@cartermp

cartermp Aug 11, 2018

Member

Linking Microsoft/visualfsharp#3108 for posterity

Member

cartermp commented Aug 11, 2018

Linking Microsoft/visualfsharp#3108 for posterity

cartermp and others added some commits Aug 13, 2018

public sealed class NullableAttribute : Attribute
{
public NullableAttribute() { }
public NullableAttribute(bool[] b) { }

This comment has been minimized.

@dsyme

dsyme Aug 24, 2018

Contributor

What does the bool[] mean? Is that used for nullability annotations for nested generic types?

@dsyme

dsyme Aug 24, 2018

Contributor

What does the bool[] mean? Is that used for nullability annotations for nested generic types?

This comment has been minimized.

@cartermp

cartermp Aug 24, 2018

Member

Not sure, actually. @jcouv ?

@cartermp

cartermp Aug 24, 2018

Member

Not sure, actually. @jcouv ?

This comment has been minimized.

@jcouv

jcouv Aug 24, 2018

Correct. Just like with dynamic or tuple element names, we serialize the information. List<string?> would get [ false, true ].
We'll add details about that in the C# speclet.

@jcouv

jcouv Aug 24, 2018

Correct. Just like with dynamic or tuple element names, we serialize the information. List<string?> would get [ false, true ].
We'll add details about that in the C# speclet.

#### Null obliviousness
Nullability obliviousness is a concept that C# 8.0 has. A null-oblivious type is one that no assumptions can be made about. Once assigned to a nullable or non-nullable variable, it is treated as if it is nullable or non-nullable, respectively.

This comment has been minimized.

@dsyme

dsyme Aug 24, 2018

Contributor

I need more information on this - e.g. examples where would C# infer null-obliviousness.

The natural corresponding thing in F# would be to have nullability inference variables. For example, the most natural implementation in F# for nullability inference would mean that even the literal "a" would initially be assigned type string | ? where the ? represents an inference variable which can be filled in with values "0" (indicating string) or "1" (indicating string | null). Thus "obliviousness" is just a dangling inference variable.

This approach is used partly because we never merge to a "common super type" during type inference. So when we have

let f (s: string | null)  = if b then "a" else s

the then and else branches need the same type. The type of s is string | null and the type of the literal "a" needs to be compatible with this.

Normally this approach to annotation inference is sufficient for F# (e.g. for units of measure and type inference) and means we won't need extra concepts like "null obliviousness" - they should just drop out naturally. A good corresponding example is that 1.0<_> is "unit oblivious")

@dsyme

dsyme Aug 24, 2018

Contributor

I need more information on this - e.g. examples where would C# infer null-obliviousness.

The natural corresponding thing in F# would be to have nullability inference variables. For example, the most natural implementation in F# for nullability inference would mean that even the literal "a" would initially be assigned type string | ? where the ? represents an inference variable which can be filled in with values "0" (indicating string) or "1" (indicating string | null). Thus "obliviousness" is just a dangling inference variable.

This approach is used partly because we never merge to a "common super type" during type inference. So when we have

let f (s: string | null)  = if b then "a" else s

the then and else branches need the same type. The type of s is string | null and the type of the literal "a" needs to be compatible with this.

Normally this approach to annotation inference is sufficient for F# (e.g. for units of measure and type inference) and means we won't need extra concepts like "null obliviousness" - they should just drop out naturally. A good corresponding example is that 1.0<_> is "unit oblivious")

This comment has been minimized.

@dsyme

dsyme Aug 24, 2018

Contributor

Reference types we cannot determine to be non-nullable will be assumed to be nullable.**

I'm not sure where "assumed to be nullable" is coming dfrom or why we would do that for F#. The only things we would assume to be nullable are things coming from .NET assemblies, and even that should be explicit assuming the assembly is annotated.

Note this would still apply even if we use nullability inference. Nullability inference variables that are not "filled in" by the end of inference would be given a default value. I believe in all situations for regular F# code that default value would be "non-nullable".

If there are counter examples we should add them

@dsyme

dsyme Aug 24, 2018

Contributor

Reference types we cannot determine to be non-nullable will be assumed to be nullable.**

I'm not sure where "assumed to be nullable" is coming dfrom or why we would do that for F#. The only things we would assume to be nullable are things coming from .NET assemblies, and even that should be explicit assuming the assembly is annotated.

Note this would still apply even if we use nullability inference. Nullability inference variables that are not "filled in" by the end of inference would be given a default value. I believe in all situations for regular F# code that default value would be "non-nullable".

If there are counter examples we should add them

This comment has been minimized.

@cartermp

cartermp Aug 25, 2018

Member

The main gist of this (which I'll clarify):

  • If C# does not see NonNullTypes(false) for a scope, it treats that scope as null-oblivious
  • If NonNullTypes is not specified, then nullability or non-nullabulity is not known
  • Once a value is assigned to a variable, it is treated as either nullable or non-nullable (e.g., assigning to string? or string), based on how it is assigned
  • var x = ... is still undecided, since type inference means that the compiler must make a decision one way or another
@cartermp

cartermp Aug 25, 2018

Member

The main gist of this (which I'll clarify):

  • If C# does not see NonNullTypes(false) for a scope, it treats that scope as null-oblivious
  • If NonNullTypes is not specified, then nullability or non-nullabulity is not known
  • Once a value is assigned to a variable, it is treated as either nullable or non-nullable (e.g., assigning to string? or string), based on how it is assigned
  • var x = ... is still undecided, since type inference means that the compiler must make a decision one way or another

This comment has been minimized.

@cartermp

cartermp Aug 25, 2018

Member

From the F# perspective, that var x = ... case is much more of a problem, since we use type inference everywhere. So I'm leaning towards us not having nullability obliviousness as a concept that we surface in user code.

@cartermp

cartermp Aug 25, 2018

Member

From the F# perspective, that var x = ... case is much more of a problem, since we use type inference everywhere. So I'm leaning towards us not having nullability obliviousness as a concept that we surface in user code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment