-
Notifications
You must be signed in to change notification settings - Fork 21
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
Introduce the ?. operator into F# #14
Comments
The null propagation operator (why give it a fancy name?) is one of those things which makes me wonder about choices made in C#. I see it as "help you to do the wrong thing", I find it useful for event handlers (but R# allowed already to introduce the null check) but in most other cases I'm seeing it introduced in code, I'd actually prefer a more explicit handling of |
And, in F#, the event handler issue is not an issue already... |
In F# any version of this operator would presumably work with both the let x = Some "s"
x?.Length and perhaps over a more general range of |
Would there be a constraint that types could satisfy through their implementation to buy into that functionality? Or would it a specific method to implemnent like how It'd be nice it were a more general purpose operator instead of one locked to a type like how |
Since
can be rewritten as:
wouldn't it be great to have an operator that basically performed a map? I'm going to borrow the "Spread-dot operator" from Groovy (http://mrhaki.blogspot.co.uk/2009/08/groovy-goodness-spread-dot-operator.html) which accomplishes the same thing:
Now, this can also be used with sequences, lists etc
I don't know how possible this is since the |
:/ I feel like this feature request is an example of how C# is starting to lap F#. |
@jzabroski do you mean F# miss this badly or that you rather not see this? I don't think it makes C# a safer and higher level language to have that feature as opposed to no null allowed on most types by default, or making it hard by default to have type members that are unitialized (which F# does since day 1 AFAIK). In that respect C# can't be fixed, at most patched, leave alone idioms of C# codebases. Speaking myself as an F# + C# user standpoint. I'm not craving for the feature, but if it comes, it is great if it would be enabled on arbitrary types, has some room to not just be the one single thing that C# does. Why I'm not craving: It also would have potential to replace usages of custom CE to deal with option monadically. I'm surprised @vasily-kirichenko downvoted the suggestion due to this: |
@jzabroski in case you are not familiar with F# 1: type A() = class end
let a : A = null // error FS0043: The type 'A' does not have 'null' as a proper value This may give you some context as to why we aren't feeling we lack the feature so much, you'd need to try F# 1 and understand some of the choices there. Nice that C# now has some embedded analyser to track nulls and slow down a bit their ever propagation (it must be very fast at the assembly level, but very expensive to have a BCL and large codebases to maintain where null soundness is > /dev/null and only soft enforced due to backward compatibility with C# 1). The concept of one language lapping another (especially on same VM where you can mix & match) has not been a concern to F# users AFAIK. A place you may contribute to the discussion if you are using F# is fsharp/fslang-design#339 which I'm looking forward to and may interest you. |
Some OO people like the https://en.wikipedia.org/wiki/Law_of_Demeter (the D in SOLID IIRC), and the null propagation operator is an invitation to infringe this law and never challenge the API choices / think too much before litering the code with if it comes, fine, I'll be importing a bit of that C# experience when I hit a NRE on code breaking the law of demeter, fixing code with a single char and not thinking much more before shipping the fix, adding to the billion dollar mistake tally. thanks @jzabroski for helping me solidify better my stance on this feature and why C# programmer likes it 🙂. |
@smoothdeveloper Many times I'm using a library or an entire framework (BCL/Xamarin) where I can't change design decisions directly. I can only write |
Yes.
I thought that about compiler performance for awhile, but there are a couple of compelling user stories:
Actually, if you read Karl Lieberherr's research papers and his book about OO programming with the Law of Demeter, he advocates using wild card patterns as substitute for needing to fully express a chain of objects. I guess this is a cultural example of how a solution was created in search of a problem, and then people only remember the problem that was raised. At the time Karl proposed the idea, code bases were tangled spaghetti code with very little architecture, and so Law of Demeter was created as a way to describe one way code bases were a tangled mess. Karl then came up with the solution, which was his pattern matching approach to function calls. Functional languages should make such code easier. Most of what people describe the D in SOLID as is copy-paste programming in the name of avoiding direct path commonalities in unrelated business requirements, since path collisions tend to result in feature composition bugs where pairwise features intersect. |
@jzabroski, thanks, the additional context is very helpful, and expanding on law of demeter also. You should upvote the feature and provide more context than "C# > F#" next time, which I feel is counter productive if your comment gets ignored or worse, it starts a flame war (which I hope you can see, is not my intent). Are you getting rid of F# codebase just for the lack of this feature, or you also have other issues with the language?
I have not downvoted the suggestions, and even in my earlier comment I'm not opposing it, just giving my feeling about tension. I'd be asking we do our best so, if it reaches F#, it does so in a more powerful manner than C#, by allowing to leverage that idiom for more than just null references. |
I have a killer issue where if I reference a C# assembly that transitively references a Resources assembly, F# compiler goes OOM. I reported it but there was no fix after much discussion. With Visual Studio 2022 going 64 bit finally, it's all too little too late. I suspect VSCode uses 64 bit processes and most F# developers just use that and don't run into this pain point as daily as I had to encounter it. Plus, VSCode uses a client/server paradigm where if the server segfaults, it just spins back up a new server in the background. I guess the other reason F# is going out the door is the original system was not well documented and had no clear business owner, and the system had a kitchen sink of F# features, like active pattern matching and partial application. At the same time, C# has made massive improvements under Mads direction. The other annoying thing with using F# in a team with a lot of C# projects in the same solution is that renaming an F# binding doesnt automatically update the Errors List in Visual Studio, which on a solution with very large projects, really lengthens the REPL cycle and getting feedback as to whether a refactoring was a good idea with 3 days left before a sprint closes. In addition, CodeLens doesnt peer through from C# to F#, so when assessing whether something is not in use, I have to use CodeLens plus Year 2000 style Control+Shift+F Find in ALl Files... Practical stuff like this is really against F# at the moment, sadly. |
just for reference: dotnet/fsharp#9175 Thanks again @jzabroski and let's not kill the good discussion about the feature (sorry for inviting digression), please contribute to issues, suggestions, PRs, etc. whenever you feel like 🙂. |
Lazy argument Evaluation of ?. in C# (useful in Logging)I want to highlight that the ?. in C# has lazy argument evaluation which has an important use case. For logging we constantly face the issue that log statements might be fed with arguments that are expensive to compute:
In earlier days, the idea in C# was to introduce lazy argument evaluation via a lambda function. With the ?. operator this can now be done
because ?. does not invoke the member but also does not evaluate the arguments of the member function . This is a great relief for C# programmers and there is no equivalent for F# programmers to avoid expensive argument evaluation when logging is disabled. F# programmers still need to use lambdas or lazy statements, which are noisy and also generate additional code in the assembly, increasing image footprint, slowing down applications. This would be my only use case for such an operator in F# but it would be very very helpful. And I think the lazy argument evaluation of an operator could have other important use cases, since lazy evaluation in F# is nowhere present as of today. |
Closing as covered by #577 |
Reopening as mentioned in dotnet/fsharp#15181 |
This is now being reopened with having RFC FS-1060 (Nullable reference types) in mind.
Making this Out of the 4 above, option+voption can carry unrestricted Which leads to suggesting the following ruleset for picking an appropriate container for the result of
In a long chain of The downside of this ruleset are the intermediate allocations for Open question: If |
We probably can map to value option instead, since they're not intended to live long in this case. Also, probably worth mentioning that in such chained value propagations we won't be ourselves flatten nested (v)options in sake of uniformity and not having special cases. i.e. instance?.SomethingWhichReturnsIntOption() // will be 'int option option' Overall this definitely needs a detailed design. My concern for options is that people will be overusing it instead of proper matching. Which is not inherently bad, but subjectively will be harder to read and debug through. |
I would expect that
|
Thank you. I'll hide the comments up above to allow people to meaningfully read the core discussion of the proposed design. No disrespect is intended - you're welcome to post the material it on another issue in this repo, or a blog elsewhere, and link it here. I want to emphasise that I appreciate your writing and you present ideas well. But I just need to keep this thread on-topic so we can make progress here. |
@dsyme I appreciate your sincere intentions. Thank you. |
I understand and respect your concern for the readers of this thread. However, given that you have read my text in its entirety, I would like to know in detail the reasoning behind your response, preferably in line with the context I provided: #14 (comment)
|
My replies above have covered this in enough detail, e.g.
Also see the design principles of the accepted RFC https://github.com/fsharp/fslang-design/blob/main/RFCs/FS-1060-nullable-reference-types.md#principles Please also note that I've requested that this thread get back to discussing the very specific design alternatives, and we pause it until @T-Gro gets back from vacation and gets a chance to digest the discussion. I don't want to discuss abstract principles further at this point. |
Sounds great. Let's discuss the abstract principles further after @T-Gro returns from vacation. I'm also glad to see that the principle is still in the RFC (Request for Comments) stage. |
@dsyme: The other two points are more about practical reasoning, I will comment below each citation separately. Resulting type
Yes. Second reason against this alternative would be the desire to eliminate both System.Nullable as well as NRT as soon as possible - and prefer working with options. Building a complicated ruleset to maintain NRTs for as long as possible seems to go against a central design of the NRT feature. This ruleset does have one small advantage over the Flattening
A small pro is that C# implicitely does flattening, because it's missing value containers (NRT and System.Nullable) do not allow nesting at all. The example at the bottom of #14 (comment) is the best reasoning for doing flattening I came up with. ??
The main reasoning is the "one way to do things" pseudo-principle, which of course has exceptions as of now already. Adding |
I'll only comment on a small portion for now. I don't yet have strong opinion about the propagation and resulting type. I do, however, regarding the ?? operator - I agree that we shouldn't have a universal one for any "null representation", and should encourage using respective defaultWith functions, as it's already a common case in existing F# code. |
A rule of Given that F# has added opt-in options to support
This does complicate the question of flattening and nesting of mixed containers a little (as in, which one to prefer of a long ident uses more different containers), but one could likely take the container type which was last (right-most). Code which opted-in for ValueOption will continue to have it, all other code can have a simple-to-explain ruleset. |
The introduction of this operator is inherently and algebraically inseparable from the discussion of how the NRTs should be. Initially, I carelessly tried to focus on this in this thread, but thanks to the advice I received, I have now separated the discussion: My comment on [RFC FS-1060 - Nullable Reference Types] discussion #339 |
Does the nullable reference types RFC not allow |
I prefer not to reduce this discussion to individual use cases, but to demonstrate a concrete counterexample that your subjectivity is not universally shared, I would like to illustrate my own subjectivity. I use Nullable reference types a lot when interfacing with Fable/JavaScript. Currently, F# does not support NRT, so I have implemented as below: type 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 then, convert JS Promise to FRP+Nullable type based on this. This is also true with C#. Some programmers would like to use NRT directly in F#, and in that case, implicit "escape" would break type consistency, potentially leading to significant issues. |
It does not. Also, since it is just a warning (= can be ignored), it would be way to easy to create null versions of anon. records or tuples and propagate them into code that would not expect them. That is why the set union of types supporting "(value types) + (types supporting nulls)" does not cover all possible types, whereas option does. |
I know the context was a bit different, but it already does. Imagine totally null-safe code let getLength myStr =
if isNull myStr then 0 else myStr.Length After I enable NRT this will give warning (since NRT doesn't work with if in F#). I'd have to rewrite thousands of lines of code from ifs to pattern matches to fix all the warnings (because warnings should not be ignored if a project is in a good shape), so I'd like to instead go further and introduce let getLength myStr =
if isNull myStr then 0 else myStr!.Length |
This is probably something we won't do.
I would also personally also push against it. If users desire, they can write their own operator for having a universal defaultwith for multiple types. |
I'm sorry, but once again, as I pointed out in the latter part here, these statements are confusing cause and effect, or aim and reason, which constitutes a logical fallacy. (Historically, both in the context of programming languages as a whole and F# specifically,) null-safety has not been guaranteed in the Nullable type operation. due to the absence of dedicated null-safe operators, while option types have been available. The new operator proposed here is the counterpart of NullableTypes (set) to ensure null-safety, and I honestly don't understand the argument that the "Aim of the NRT feature is not to" ensure null-safety when specifying its characteristics. |
In the latest update to the C# type unions proposal it looks like there's a chance an (If they actually did end up adding it to the BCL, it could probably also be worth waiting to see whether they gave any special treatment to it in C#, like C# already does for |
Common Unions in the csharplang/proposals/TypeUnions.md also suggests Result type other than Option (which makes sense). Variety/selection of types of C# or .net framework as the foundation is real also reasonable, and the approach of unifying all types into the preferred option types in F# (or containing the other unpreferred types in the system boundary) seems quite unrealistic, especially from a medium- to long-term perspective, I think. |
I think it's good to keep that in mind by making the I don't think we should actually wait for it before adding |
I did encounter the same situation when applying nullness to the FSharp compiler itself, this will definitely be a code-migration concern. I used two solutions, which I used and (subjectively!) consider more appropriate than a language-wide addition:
|
I am against adding this operator for the following reasons:
People are not understanding the concision benefit of F# here. It is nice that F# is a concise language but we already have concision here in that there is already a commensurability of conceptual complexity to written complexity (via |
I think we should emphasize that this lang suggestion has been revived in the context of Nullable Reference Types support. Without strict null checking, code accessing Without this context, e.g. just considering options/voptions, I would be on the side with you, @charlesroddie . |
@T-Gro Sorry for the slow reply. What you are describing is a stopgap feature to help people with using non-NRT-tagged libraries and a lot of nested properties. If that is the case then if implemented, can it be restricted with a feature switch so that only this subset of users can turn it on? And would you propose having an removal date when non-NRT-tagged libraries are sufficiently rare? From what I see this date would be in the past as almost everything is NRT-tagged these days. |
As of now, any project targeting just net472 or netstandard (and NOT multi targeting netcore TFMs as well) will not be in a position to have good nullable annotations, because they are not maintained in the corresponding BCL either. I think we will have to admit that non-annotated code will be around for many more years. |
this is kind of a shame and orthogonal (to keep things "focused" assuming it is the best thing in the world, at all times, at all cost), but I think it is worth for F# team to bring to BCL team the complaint that desktop framework codebases also deserve null safety, they can consider adding the annotations to a desktop framework update, for sake of rewarding customers that are windows only, and made .net the success it is today and still support code that isn't running on new runtime. @charlesroddie, I'm personally not opposed to the operator (or equivalent) being brought to F#, in C# it is concise, sure it is used (by me and others) in sloppy code, for codefixes that is only one or few bytes and not most readable when speed reading such code, but this is reality of codebases, and the operator is endorsed by the C# users. It is not because a facility in the language exist that we encourage abusing it. The truth is, null reference type is the closest to "zero cost abstraction" F# can support, and having an approach for null/default propagation that works with those, as well as arbitrary types ( We could put the operator definition in an "unsafe" module, for which it is relatively easy to add an analyzer, to achieve what you need. This is just to mitigate my earlier feedback, I'm using the operator in C#, shame on me 🙂, I know in F#, if expression, better code design, etc. makes it less useful, but I'm not radicalized against. Question is, is there a need for the compiler to be updated for supporting this in an external library, while yielding zero cost abstraction and generality over @charlesroddie, if the design guideline are edited to discourage abusing the operator, while describing how to use it or make it work with arbitrary types, would you feel it doesn't compromise F# beyond repair? |
This operator does need a parser change to make the syntax smooth, e.g. not having to use a lambda for the member access. And in chained scenarios, not having to put in parens. let achievableViaOperator = data ?. _.Member
let afterParserChange = data?.Member Similarily with |
Going out of our way to help legacy projects in a way that hurts modern F# is a really bad approach. Legacy F# will continue to work for legacy projects. If legacy projects want minimal change, they can turn NRTs off.
Every library that I have seen that is actively maintained has moved to multitargeting or alternatively to dotnet6+ from netstandard. Failure to do so would annoy all users (w.r.t. NRTs but also trimmability) so there is a lot of demand for libraries to do this.
F# is not cobol.net so F# should have no more interest than other dotnet languages in pushing for support of legacy frameworks. There could be a miswording here. dotnet6+ targets desktop platforms fine. You can also make a windows-only application on dotnet6+ if you want. The F# community doesn't need to reward this as it is not our goal to make people buy more windows licenses. Microsoft general approach here with dotnet is good: focus on the best experience for modern dotnet. Now that dotnet8 supports every dotnet target, previous platforms (mono, netfx) don't need to be given new features since users of those platforms can just update. Keeping netfx and mono up to date with new features would hurt the future of dotnet as it takes energy away from development. Both legacy frameworks are just inferior to dotnet6+.
I would trust myself personally not to use it but would want a way to enforce that no one ever adds it to the codebase in our team. This would need to be automated. Also these guidelines would then become more confusing to beginners ("don't use it unless x" and they would need to understand what "x" is which is some extremely niche use case). |
Sure, I can make similar statement about going out our way to use F# instead of C#, it is really "bad approach" (according to some), yet, we all have, to some extent, try to make progress, in ways that are at our grasp, and improved situation for F#, despite "really bad" feedback. My statement is just more about Microsoft honouring those customers that basically funded .net from day one, and will continue to do so, whether or not desktop framework support is disincentivized. We don't have to look at it 100% from our limited perception about ROI measured on FED denomination, we can have holistic understanding and demean this as "really bad approach", based on $$$ROI metric only, IMO, is a shortcoming too.
Anything for good of dotnet codebases, holistically, is not bad to just express, but I'll not push further here. cobol.net analogy is "really bad approach" 🙂. Microsoft approach is overall good, maybe not double plus good good. No NRT annotation on framework is bad decision (as it doesn't incentivize anything, just bad code for discriminated against codebases). I'm singling out mono (I assume codebases that are mono specific are in less amount than .net desktop framework), but NRT on assemblies, it would just mostly work, could be nudged independently than what I'm doing. I was just saying to MSFT F# team (not you), "please state once to BCL team, if/when engaging about implementation of this suggestion, concern that no annotations for .net framework harms dotnet codebases".
If under a specific module, possibly another assembly, I think it would be minor for people interested in using the analyzer toolkit and needing it done.
advanced topics can be marked in the guidelines with a bit styling, I rather have them comprehensive and touching on ethos/aspects, even if it is not for every beginner to grasp them 100%. Thanks for all your feedback and hearing me. Maybe you can reconsider being neutral rather than 👎 on the suggestion, and I'll tackle the other radicalized "no nullness operator in F#" as they come back in troops to trample my words 🙂. I think the suggestion is legit, and lower priority than if we were dealing with C#, being neutral or 👍 for it, feels better to F# of today, to me. |
That's a great discussion. I think, at some point in the next release window, we shall spend time to write an rfc for that feature (covering custom operators, types which are covered, alternatives, etc), it will be much easier to argue about and discuss specific code. We should also spend some time experimenting with existing OSS projects, trying to enable nullness there and see how many new diagnostics do we have. |
This proposition fits very well into the direction of custom analyzers which we are planning for one of upcoming releases as well. |
Discussion in Fable has been initiated. Since JS/TS nowadays actively leverages In fact, from Fable perspective, .NET 9 |
Submitted by John Azariah on 11/23/2015 12:00:00 AM
16 votes on UserVoice prior to migration
Since we allow the . operator to reference fields and properties of objects in F#, we're faced with the same problem of null checking that plagued C# until C# 5.
The C# 6 'elvis' operator propagates nulls in a succinct way, and I think that working with objects in F# will be similarly simplified if we introduce it here as well!
Original UserVoice Submission
Archived Uservoice Comments
The text was updated successfully, but these errors were encountered: