[RFC FS-1060 - Nullable Reference Types] discussion #339
Replies: 59 comments 21 replies
-
There are other examples, e.g. compare
and
Here the reduction to perform on Requiring parenthesization may be feasible but definitely fiddly to implement
|
Beta Was this translation helpful? Give feedback.
-
Other options might be let len (str: Nullable<string>) = ... // for reference types compoiled as `string`, for value types Nullable<_>
let len (str: string or null) = ...
let len (str: string || null) = ... |
Beta Was this translation helpful? Give feedback.
-
Do option types "None" still map to Null in C# world? And therefore Some t maps to this new Non Nullable? That would be a breaking change in many places in my code base |
Beta Was this translation helpful? Give feedback.
-
@jamessdixon You can read about this a bit here: https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1060-nullable-reference-types.md#interaction-with-usenullastruevalue In short, |
Beta Was this translation helpful? Give feedback.
-
My feedback: I really like the "?" notation because it is concise, and I'm already used to it from C#: Unimportant side question: would we would have to If below is also valid, then does that mean we could also substitute I really like the twist that the emitted warnings can also be tuned to emit as errors. I would definitely want that to be the case all the time, but I can understand why it might not be the default. Nice to have the option though. |
Beta Was this translation helpful? Give feedback.
-
No, it would have to be available without opening that namespace.
Perhaps. We're considering how to rationalize nullable reference types with nullable value types ( |
Beta Was this translation helpful? Give feedback.
-
@dsyme We'll also need to consider nullable operators: https://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/symbol-and-operator-reference/nullable-operators I think I'd discourage their usage, but it could feel weird that they didn't work with nullable reference types. |
Beta Was this translation helpful? Give feedback.
-
@cartermp Nullable operators are primarily present for query expressions. Indeed Nullable in C# is primarily there for nulls in databases and LINQ It may well raise lots of questions about nullability and database queries and database ORM mappers. And type providers too |
Beta Was this translation helpful? Give feedback.
-
Yeah, I'm not keen on them being used outside of that context (lacking any data, my impression is they're rarely used already). But it'd be good to keep track of nonetheless. |
Beta Was this translation helpful? Give feedback.
-
A note on inference subtleties... In current F#, code such as let f x = match x with null -> 1 | _ -> 2 places a > let f x = match x with null -> 1 | _ -> 2;;
val f : x:'a -> int when 'a : null That is, in the absence of other information F# 4.5 infers a definition-site annotation on the type variable itself. If the input type is known to have a use-site nullable annotation, then it seems reasonable to allow this: let f (x: string?) = match x with null -> 1 | _ -> 2;;
val f : x:string? -> int This is not so bad and is likely the behaviour we will expect, but it requires type annotations in a way that is unusual for F# code: adding the type annotation fundamentally changes what the code is about (from definition-side nullability to use-site nullability). This is somewhat consistent with how units of measure work in in F#. Note that in a fully fledged version of a use-site nullness feature you would expect > let f x = match x with null -> 1 | _ -> 2;;
val f : x:'a? -> int However this would be a breaking change and in any case nullable annotation code can't be generic like this. If we did allow it (eg. in inlined code) then a type annotation would be needed: > let inline f (x: 'T?) = match x with null -> 1 | _ -> 2;;
val f : x:'T? -> int Indeed this should be allowed (it is compilable once 'T is known to be either a struct or a not-struct) > let f (x: 'T? when 'T : not struct) = match x with null -> 1 | _ -> 2;;
val f : x:'T? -> int when 'T : not struct
> let f (x: 'T? when 'T : struct) = match x with null -> 1 | _ -> 2;;
val f : x:'T? -> int when 'T : struct |
Beta Was this translation helpful? Give feedback.
-
A note on inference... In the prototype the following gives a warning because let y0 = isNull null I suppose we should use In contrast the following does not give a warning: let y0 = isNull (null: obj?) |
Beta Was this translation helpful? Give feedback.
-
A note on From CompilerLocationUtils.fs with nullable checking on:
Here is RegQueryValueExW accepting nullable strings or not? |
Beta Was this translation helpful? Give feedback.
-
I'm currently working on compiling A note on val unbox : value:obj? -> 'T
val box : value:'T -> obj?
val tryUnbox : value:obj? -> 'T option However I actually feel that for most F# programming the type |
Beta Was this translation helpful? Give feedback.
-
Now compiling up FSharp.Compiler.Private using nullness checking prototype. Just looking at the first few messages. Here's one example of a place where we need to consider how we will update FSharp.COre: type StructuredFormatDisplayAttribute =
inherit Attribute
new : value:string-> StructuredFormatDisplayAttribute
member Value: string SHould |
Beta Was this translation helpful? Give feedback.
-
Mixing optional params and nullable params with the ? in member definitions is going to lead to a lot of confusion, as in the optional syntax:
|
Beta Was this translation helpful? Give feedback.
-
Yes it's partly for my own purposes right now, the |
Beta Was this translation helpful? Give feedback.
-
I don't know if this is still a thing, but I just saw the talk Philip gave to NDC about this. In the talk the first question he gets is why not treat them as Option values instead of building a whole new thing. He answers that it will be a lot of work, especially in the interop layer, and I can see that. But I just wanted to say that it really feels like a wasted opportunity to build a new thing when we already have the Option type that, for all intents and purposes, does the same thing. Regarding the interop code, when handled a null now it will already crash right? There is just a convention that you won't be handled a null, it is not enforced. So if you treat the nullable type as an Option it would be possible to do an Option.bind on all the parameters and when a null is found it will just crash like it would do now. There are probably smarter ways of doing this, I am by no means a great programmer or anything. As for backwards compat, these types are only in C# 8 right if I understand correctly? So they are only found in the most recent .Net versions, can't we do a version check somewhere. I am not an expert by any means, just my 2 cents. |
Beta Was this translation helpful? Give feedback.
-
@boeremak Implicitly treating any nullable reference type as an Additionally, the way that the option type is compiled into .NET metadata means that it must be a nullable reference type, since it uses |
Beta Was this translation helpful? Give feedback.
-
@cartermp I did some more reading about the C# rationale and it seems they want to transition to a place where F# already is, namely things can't be null and have to be initialised when you use/instance them. At least that is what I got from reading this: https://devblogs.microsoft.com/dotnet/nullable-reference-types-in-csharp/ Is that correct? Another thing, seems like quite a change especially if you keep the IL output the same. So it is purely enforced in the tooling? A I did some more reading and these are good points: fsharp/fslang-suggestions#577 (comment) And is exactly my thinking. Edit: Another option: why not keep it the way it is, everyone already does null checks ( i hope :) ) when things come from C# and when it comes from F# it can't be null anyway and just forgo this completely? Maybe add some smarts to intellisense to say "this has been compiled in C# 8 so you probably don't need to do null checks on this". I don't know the feasibility of this though. One think I would really dislike is for F# to accommodate C# in a way that makes the core language cumbersome to use. I really do like the language. Edit2: If this has already been discussed, I am sorry I just saw the talk today. |
Beta Was this translation helpful? Give feedback.
-
I suggest reading the Motivation and Principles sections detailed here for a thorough rationale of the feature from an F# and .NET perspective: https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1060-nullable-reference-types.md#motivation Much of the rest of the spec is concerned more with details on what needs to be done and what they design will be, but this prelude is the framing for the feature. |
Beta Was this translation helpful? Give feedback.
-
but I think it more forward-looking |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
The indications are that F# may not support NRTs at the launch of .Net 5 and that's going to be a large problem for F# as this is the recommended timeline for libraries to adopt NRTs. The suggestions in the RFC are good, but not all are necessary to support in V1 of this feature. It's important to get this feature to a minimal spec to reduce the effort to get V1 out. Suggestions for cuts to the RFC / moves to a "Future" section"Partial alignment of nullable value types and nullable reference types."This can be cut. Value types don't need a Omitted NonNull in pattern matching
Having Null ambivalence (obliviousness)If necessary this can be dropped from V1 since it could add a lot of complexity in implementation. Especially droppable if there is a rollout phase in F# where the feature is opt-in: early adopters can see if obliviousness causes significant difficulty. Unresolved question: nullability warnings on a per-file basis?This can easily be scoped out of V1. Unresolved question: UseNullAsTrueValueThis is an F# flaw and not C#/.Net's responsibiltiy to fix. Have these type exposed as NRTs and have a migration path for Option. |
Beta Was this translation helpful? Give feedback.
-
I think we should adopt the same syntax as https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1092-anonymous-type-tagged-unions.md with the required parentheses like |
Beta Was this translation helpful? Give feedback.
-
One major reason why I'll probably never adopt F# is a wide range of C# libraries that use NRTs to mark "optional" properties. F# uses Option type but it seems useless and counter-intuitive when you consume C# libraries just forcing you to use I think it would be interesting if NRTs could be interpreted as |
Beta Was this translation helpful? Give feedback.
-
For me it’s never been that much of an issue turning a potential null into F# friendly usage, it’s not very often and when I do encounter it it’s just a Option.ofNull etc. I suppose it depends on the domain?
… On 21 Jun 2022, at 00:06, bugproof ***@***.***> wrote:
I think if someone consumes a C# library that makes use of NRT from F# it would benefit F# users if they were treated as Option<T>. NRTs were added to C# only because there was no way to tell if a property should really be null but it doesn't do much else.
In F# there was always Option<T> type which is and will always be much better than NRT and anything C# team will come up with in the future like required keyword.
Would you mind to expand on the other reasons for never adopting F#
I find that bridge between C# -> F# too convoluted. I'd like to use Option<T> everywhere in my code base but I know it's simply impossible because there will always be some C# lib I will need to use and it will feel just like writing C# again but with a weirder syntax. If there was a way to write code without using null ever again just like Rust but .NET ecosystem that would be nice.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
Beta Was this translation helpful? Give feedback.
-
ILMerge does that.
…Sent from my iPhone
On 25 Jun 2022, at 11:06, bugproof ***@***.***> wrote:
It would also be nice if F# types could be consumed from C# without a dependency on FSharp.Core. Maybe if there was a tool based on Mono.Cecil or dnLib that removes the dependency and converts Option<T> to NRT that would be great. I maintain one library that has a lot of data only classes and a lot of optional properties. If I could re-write it in F# and make it usable from both it would be a win-win for everyone.
Or maybe write a tool that does the opposite? Generate F# compatible packages that convert NRT/required stuff to Option<T>. That would be interesting.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.
|
Beta Was this translation helpful? Give feedback.
-
To work around the issue, I did the following: https://github.com/ken-okabe/vanfs?tab=readme-ov-file#nullable NullableTtype NullableT<'a> =
| Null
| T of 'a
member this.Value
= match this with
| Null -> failwith "Value is null"
| T a -> a
let NullableT a =
match box a with
| :? NullableT<'a> as nullable -> nullable
| _ -> T a Usagelet nullable1 =
Null
let nullable2 =
NullableT "hello"
log nullable1
// Null
log nullable2
// T hello
log nullable2.Value
// hello |
Beta Was this translation helpful? Give feedback.
-
Here is a comment on F# RFC FS-1060 - Nullable Reference Types. The concerns I express below do not hinder or affect the implementation of Nullable or NRT itself, which is the goal of this RFC. However, they are related to and could potentially impact the RFC-ization of Introduce the ?. operator into F# #14 (@T-Gro), which is related to this RFC, and its subsequent implementation. KISS principleF# is my favorite language. I like that it's extremely versatile and practical as part of the .NET ecosystem, and that it's not as difficult for beginners as Haskell. I actually used Haskell for a while, but I got tired of the complexity, such as having to use do notation that depends on the Monad structure just to express a sequence, and the excessive use of extensions to disguise the language's own fundamentalism. I prefer the simplicity of F# as my primary development language. Unlike object-oriented languages, the great thing about functional languages is that there are no unnecessary artificial premises like class structures. Of course, it goes without saying that I believe the excesses of Haskell's Do notation are an unnecessary burden on humans, and that ML or F# is the right landing point. In any case, I hope that F# will continue to be a simple language that is easy for beginners to approach, without unnecessary premises, as KISS principle My ConcernHaving said that, I have a concern that the "principle" in the RFC is an unnecessary premise and redundant, introducing complexities. According to the proposal/suggestion: Add nullable reference types [RFC FS-1060] #577, the "Proposed Design Principles" have existed since the first Proposal comment. At the time this first comment was made, it was before .NET metadata produced by C# 8.0 indicating when types are "with-null" in the .NET framework 5 or later, so I can imagine that some kind of clue was needed. In fact, it is clearly stated there that:
Since then, there seems to be no evidence that the validity of this principle itself has been properly verified and agreed upon by the community. Nevertheless, there are mentions that the principle is "established" for unknown reason. The Minimal, Objective PolicyI believe the following statement in the RFC is a clear and sufficient implementation policy for everyone. It is minimal, easy to understand, and leaves no room for objection. (KISS principle) MetadataRepresentation of nullable types in .NET Metadata
The Redundant, Subjective "Principle"However, if we accept the policy for interoperability with the .NET Framework and its implementation as C#, all references to the Principles of RFC become redundant. In particular,
"The value" is pretty much subjective and depends on people, accordingly, it is not a concept that depends on the implementation of types. Whose values are these? At the very least, I do not intend to share these values.
This is a completely unnecessary constraint. It is up to the programmer to decide how to use the newly introduced types, and this is an unnecessary premise, as I emphasized at the beginning. And this Principle has not affected the implementation of the Nullable type itself.
Same as above. Why does the F# language need to define the "concern" of the user? This is also irrelevant to the implementation of the type introduction.
This is exactly right. It shows a positive use case, as opposed to a negative list of things not to do. This is a natural motivation for anyone to accept. What if Fable programmers are enthusiastic about using NRTs extensively in their projects? Why should this freedom be restricted? So, these are the unnecessary premises and introducing complexities. (breaking KISS principle) Complexity of the RFC DocumentThe "Principle" introduced seeds of complexity, which then rippled throughout the entire RFC document, ultimately resulting in an extremely convoluted and complex document. From a broader perspective, the root of this complexity can be distilled to a single foundation:
If we remove this premise, all the complexity disappears. Constraints on "Abnormal" ValuesIntroduce the ?. operator into F# #14 fsharp/fslang-suggestions#14 (comment) @dsyme states, based on this RFC Principle:
Of course, the statement is seen as reflecting the root of all complexity:
however, there is some substantial escalation from the RFC Principle, Accordingly, it would also be natural that, as far as I can see in the thread, multiple members including myself have expressed concerns about his statement. Initially, I overlooked the principles written in this NRT RFC, actually considering that they would not affect the implementation or actual user usage, but after reading his comments, that optimism no longer applies, since the type preferences are abnormal and never principles, as they are a human preference, not a programming language one. I believe that prioritizing types and labeling them as "abnormal" is meaningless and unnecessary. It introduces needless complexity to F#. Why not treat all types equally and allow programmers to perform implicit type conversions? This would eliminate arbitrary rules and make the language easier to learn and use for everyone. Of course, I should be humble about my potential ignorance as I'm not following every detail. To be fair, I must mention that @dsyme has also carefully and responsibly expressed his concerns as follows:
Again, I don't have a precise grasp of everything, so I need to be careful with my statements. However, the validity of this claim should be assessed based on the results of the ongoing NRT implementation, rather than the current/legacy situation. For instance,
This is a typical argument based on a false premise. The current Nullable, without NRT, is extremely limited and not general-purpose, so programmers are simply guided to use Option, so far.. This argument does not consider that if this limitation were removed, the situation would evolve. In other words, I suspect that the claim of Nullable types being "abnormal" due to their current immature state is a logical tautology and therefore invalid. The idea comes from the following background perspective. Finally, The Background Perspective of NullFinally,
The following is my perspective on the "F# has always taken the approach that F#-defined types do not have null as a normal value, while .NET-defined types do. This is a longstanding tension between F# programming and the .NET ecosystem." It's a bit long, so please read it if you're interested. Click to expandThere is a historical context for why null is so strongly avoided in programming: Null pointer
History
Null values have a notorious reputation in the programming world, often leading to runtime errors and unexpected behavior. In response, functional programming languages like Haskell, OCaml, and F# have adopted a different approach to value representation, favoring the Option types, represented as The Option types, while often perceived as complex for beginners, can be conceptualized using the analogy of lists or arrays . Consider a container structure that can either be empty, represented by
Appearently, the Option types can be useful, but they can also lead to unnecessarily complex structures. Consider a Cell . This can be represented by
In a case the cell is empty, This can be represented by
This system works so far. However, the List or Option type can be easily nested such as:
corresponds to: or
corresponds to: What we need is not a nested Cell that is weired and meaningless but simply an empty Cell . TypeScript cleverly avoids the complexity of nested Option types by employing the Nullable types instead. Let's explore an example of a VSCode Extension that requires extracting the text from the active text editor.
The TypeScript compiler is issuing errors and warnings. The problem is:
In JavaScript, To visualize, it's like this! Since VS Code users can close all tabs in the editor, The situation with So, the proper type should be as below: This is the Nullable type and what we really need. In Functional Programming, everything is an expression or operation, at least, in principle. When constructing expressions for mathematically consistent algebraic structures, it is essential to employ the correct types and their corresponding operators . The concept of null references being a "billion-dollar mistake" stems from the lack of a well-designed null type and corresponding operators for programmers to use effectively. In this case, we should use Optional chaining (
Accordingly, the TypeScript code with the error should be fixed as below: While the naming convention "optional chaining" evokes Option types, its actual behavior differs from nested Option types. Unlike Option types, which allow values to be either Some(value) or None, nullable chaining deals with values that can either be valid values or null. Therefore, "nullable chaining" might be a more accurate and descriptive name. The "billion dollar mistake" associated with Null is a real issue that has caused countless problems in software development. However, it has also become a myth that perpetuates a misunderstanding: that the Null value itself is inherently evil. The real issue with Null is not the value itself, but the lack of proper operators to work with Nullable types, as demonstrated by languages like TypeScript. The "billion dollar mistake" of Null essentially boils down to this: a failure to maintain a mathematically sound algebraic structure. In contrast, Option types were designed from the ground up with this rigor in mind. There is no inherent superiority, inferiority, or abnormality in the mathematical structure of Nullable and Option types. The only difference is whether or not a mathematically consistent implementation has been provided. Furthermore, it's unclear why the functional programming community didn't initially implement both:
as equal counterparts. I don't have a definitive answer to this. However, it can likely be attributed to the strong aversion to Null itself, rather than any rational reason. This emotional response is evident in the current proposal, where the focus is on eradicating Null rather than exploring alternative solutions. In conclusion, the "billion dollar mistake" of Null was not the Null value itself but the lack of a mathematically sound implementation. While Option types have historically been favored in functional programming, Nullable types can be equally valid with proper operators and a consistent mathematical structure.
This design "principle" or "preference" is not based on any mathematical validity. We can therefore conclude that C# or the .NET framework, which do not have such biases, are more sound in their approach, provided that Nullable types come with the correct set of operators. FAQ: Nullable Types vs. Option Types in Functional ProgrammingQ1: Don't Nullable types lead to NullPointerExceptions? Q2: Aren't Nullable types less explicit about the absence of a value? Q3: Don't Option types offer a more unified API for working with optional values? Q4: Aren't Option types better suited for functional programming due to their monadic nature? Q5: Can't Option types handle errors more seamlessly than Nullable types? Conclusion |
Beta Was this translation helpful? Give feedback.
-
Discussion in Fable has been initiated. Since JS/TS nowadays actively leverages In fact, from Fable perspective, .NET 9 |
Beta Was this translation helpful? Give feedback.
-
Discussion for https://github.com/fsharp/fslang-design/blob/master/RFCs/FS-1060-nullable-reference-types.md
The biggest user-facing change in the RFC is the discovery that syntax like this:
Would be a breaking change. For example, this code is valid F# today:
And there are likely other variants of this that people could conceivably have in their code.
Thus, we are currently thinking that we will match the C# syntax:
Since it is not a source-breaking change.
Beta Was this translation helpful? Give feedback.
All reactions