-
Notifications
You must be signed in to change notification settings - Fork 785
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’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Nullness issue - unclear how to deal with 'T | null interop #17734
Comments
Adding C# code for reference: var x = ctx.Request.ReadFromJsonAsync<int>().Result; // x is of type int
var y = ctx.Request.ReadFromJsonAsync<string>().Result; // y is of type string? Declaration:
|
|
This is a consequence of the C# code misleading about the return type - because the returned value is not nullable in the System.Nullable sense (as it would be in C# in a non-generic context), the nullness simply isn't there at all if instantiated by value type. Without knowing the implementation, one could assume that the C# code returns null for reference types and default for value types - without any indication to the caller that a default value was chosen. It will not crash with NRE, but it might be another source of issues if the absence of information is not communicated via the type system. Intention-revealing workaround would be to wrap this method in a dedicated generic call restricted to value types, and dealing with absence of information in value types explicitly. I do understand that C# does allow (T | null) in generic code without it having the right meaning for value types ;; a compromise taken to allow coexistence of NRTs and value types. I do not believe F# should repeat exactly the same, at least not without an approved suggestion. |
My suggestion for now would be to expose two methods: Deserialise for reference types returning T | null. A bigger ask would be the unification of NRTs and NVTs in the type system already. However, since they have different representations at runtime (none for NRTs, System.Nullable for NVTs) this would still not work across interop boundary. But assuming an approved suggestion and RFC, I think an implementation for F#-inlined generic code could make this work (ignoring the efforts for now). |
I second that |
I have discussed it with Don numerous times, and it was decided that we don't want to do it. We don't want to encourage people to use nullable reference types let alone unify nullables under one syntactic structure. It will make it harder to understand what is going on and will be hiding concrete types behind the new construct. In future this can be partially (on the declaration and params side) remediated when we introduce anonymous unions. That said, I suggest creating a new separate language suggestion w.r.t constraint-by-default, S.Nullable<_> and their interop/unification with nullable references feature. |
Syntactically, I do get the ask for "or" in constraints. However, this does not have an IL equivalent for codegen. |
What could be done is removal of the nullness if a generic type allow " | null" is instantiated with a value type. However, that means also hiding a flaw in the API (the API basically admits that missing information is possible, you just do not know how it is represented at runtime and likely cannot even detect it), whereas current behaviour gives a warning - not with the appropriate wording blaming a generic construct, but a warning at least. I do not see how F# would support authoring such code, since F# even explicitly makes sure that " | null" in authored generic code automatically enforces a reference type constraint. |
I think this has to be done anyway, since there should be a way for user to consume C# Serializer API anyway as it's currently not clear how to deal with
Can't we relax enforcing one constraint? I mean keep |
The added nullness should be subsumed, if you annotate the binding to be without null With the constraints it gets tricky - as soon as it is not enforced, it could lead to invalid IL when the generic type/method is instantiated. (imagine the generic code having a |
Think of |
Before F# 9 it derived
This works, thanks, probably should be documented
I agree, that's why I suggested to keep It is still unclear how to handle |
This was already decided during designing of this feature, so if that's to be change it has to be a fully-fledged language suggestion. |
@abonie sorry, what exactly was decided during design of this feature? How do I write the JsonSerializer.Deserialize wrapper without warnings? Is the "already decided" solution to disable warnings for such code with #nowarn 3261? let tryDeserialize<'T>(value: string) =
try
JsonSerializer.Deserialize<'T>(value) |> Some
with ex ->
None |
Constraint inference was part of design decision, suggestion is needed to omit it. |
Representing both nullable reference type and value type in IL is tricky, and what C# does is it just pretends that null is not there from the flow analysis standpoint. In F# | null is possible only with reference types, hence the constraint is for correctness. |
With this issue I don't suggest removing constraints or do any other particular suggestion, it is to point out that currently it is impossible to normally use popular .NET libraries (which is undesired behavior), and this ticket shouldn't be closed until the issue is resolved. I agree that the concrete suggestion "how to fix it" might go into suggestion repo, but it's not a reason to close the unresolved issue. |
It is currently not actionable for us here. It needs a specific suggestion of what the behaviour currently and what it should be. Suggestions repo is an ideal place for something like it, as most of the discusssions happen there. |
Ok sounds like "currently not a bug", but before closing the issue users like me should get an answer - what to do in current situation. Is the currently suggested solution to disable nullness warnings? |
I would say so, or use optional types where possible |
I do think there is an issue here with the way F# consumes the metadata for generic // Tooling shows x: int | null. This could never actually be null when 'T is a value type.
let x = Unchecked.defaultof<HttpContext>.Request.ReadFromJsonAsync<int>().Result
// Tooling shows y: int | null. No warning is given.
let y = x + 2
// Tooling shows z: int. No warning is given.
let z = 2 + x Contrast that against let x: string | null = null
// Warning FS3261. y is inferred to be string | null (even though it will never actually be null due to how + works in F#).
let y = x + ""
// No warning. z is inferred to be string.
let z = "" + x At the very least, it's confusing. I can see the logic behind the C# approach: int a = M<int>();
int? b = M<int?>();
int? c = M<System.Nullable<int>>();
string d = M<string>(); // Warning.
string? e = M<string?>();
T? M<T>() => default; I think it would make sense for F# to do the same thing here — or at least for the tooling not to show impossible (and inaccurate) types like |
I will check that. What remains impossible is authoring null-allowing generic code supporting both reference and value types (authoring does include wrappers). Which does not prevent using them with concrete non-generic instances. |
(if a suggestion is created - a strong point should be raised against modelling the absence of information with If the solution would be to convert such null-allowing generic code into option/voption, the modelling problem disappears) |
I would add that authoring null-disallowing generic code is also impossible. Any generic code that consumes C# library with generic // possible solution which is currently impossible
let tryDeserialize<'T>(value: string) =
try
match JsonSerializer.Deserialize<'T>(value) with
| Null -> None
| NonNull x -> Some x
with ex ->
None |
That is correct for the new nullness checking functions. The proposal could include some relaxed conditions + optimizations for built-in functions specifically for consumption of null-allowing unconstrained generic code. |
Issue description
Some C# public API's currently expose
'T | null
in signature, while F# adds additional constraints per RFC. With F# it should be possible to:It seems that neither of those points is posssible with .NET 9 RC1. Real application is Oxpecker library where I'm unable to express deserialize method signature to match C# underlying call.
Choose one or more from the following categories of impact
null
constructs in code not using the checknulls switch.null
,not null
).Operating System
Windows (Default)
What .NET runtime/SDK kind are you seeing the issue on
.NET SDK (.NET Core, .NET 5+)
.NET Runtime/SDK version
No response
Reproducible code snippet and actual behavior
Possible workarounds
I couldn't find any workarounds
The text was updated successfully, but these errors were encountered: