Introduce the ?. operator into F#
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 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 null value than operator which is actually hard to see (since it is always in middle of two other names).
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 nullable and option type as well, so
let x = Some "s"
x?.Length
and perhaps over a more general range of x.HasValue, x.Value types?
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 .Item enable array indexing?
It'd be nice it were a more general purpose operator instead of one locked to a type like how (::) is
Since
let x = Some "s"
x?.Length
can be rewritten as:
let x = Some "s"
x |> Option.map (fun x -> x.Length)
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:
let x = Some "s"
x*.Length
Now, this can also be used with sequences, lists etc
type Person = { name : string }
let people = [{ name = "Alex" }; { name = "Sam" }]
let names = people*.name
I don't know how possible this is since the map method exists in a module, not on the object itself.
:/ 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: (if isNull then a else b) is idiomatic and very explicit (and allows b to be something else than propagating nulls).
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:
https://twitter.com/kot_2010/status/1156096657609646080
@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 https://github.com/fsharp/fslang-design/discussions/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 .?, this feature has low priority to me.
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 match ... with null -> () | x -> match ... with null -> () | x -> ... where ?. would have been more appropriate.
@jzabroski do you mean F# miss this badly
Yes.
Nice that C# now has some embedded analyser to track nulls and slow down a bit their ever propagation
I thought that about compiler performance for awhile, but there are a couple of compelling user stories:
- F# code calling C# code (same solution) - this is my most common pain point, and why I am getting rid of a large F# code base (was previously over 11k SLOC, now under 7k as I've been rewriting it)
- F# code calling arbitrary nuget package code - for example, who needs to know what Entity Framework Core is written in, the only thing that needs knowing is an entity may be null, so an entity path expression may fail when accessing an entitie's null properties.
- Unifying behavior of option and nullable types
Some OO people like the https://en.wikipedia.org/wiki/Law_of_Demeter (the D in SOLID IIRC), and the elvis operator is an invitation to infringe this law
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?
@Happypig375
where ?. would have been more appropriate.
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.
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 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.
I have a killer issue where if I reference a C# assembly that transitively references a Resources assembly, F# compiler goes OOM.
just for reference: https://github.com/dotnet/fsharp/issues/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:
void Log(string a) { Console.WriteLine(a); }
string expensive() { return "expensive computation"; }
Logger logger = null;
logger?.Log(expensive()); // expensive is not evaluated when logger is null !
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
- simple
- very succinct and with a minimum of visual noise (log statements should not clutter the code)
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 https://github.com/dotnet/fsharp/pull/15181
This is now being reopened with having RFC FS-1060 (Nullable reference types) in mind. Based on the discussion above, the suggestion would be to support 4 different container types:
option<T>voption<T>System.Nullable<T>T | null(nullable reference types. Erased at compile time, nonexistent at runtime)
Making this ?. feature not just "null propagation operator", but rather a "missing value propagation operator" (definitely accepting a good name covering it)
Out of the 4 above, option+voption can carry unrestricted T.
System.Nullable can only carry value types, and T | null can only carry types supporting null. Which apart from value types also excludes selected F# types, like F# options and tuples.
This means that for a long identifier like nullableDtoType?.TupleProperty the type of the outer container (in this case, T | null) cannot be maintained.
Which leads to suggesting the following ruleset for picking an appropriate container for the result of ?.:
option,voption keeps the container
Nullable<T>, T|null maps to option<T> at first conversion point, no matter the type of the ?. 's RHS (right-hand side of the operator). If the RHS is itself an option, it does not flatten it - it will become an option<option<T>>.
In a long chain of ?., like A?.B?.C?.D, the overall container type is only determined from C (the last LHS, left-hand side).
All the intermediate ?. do not matter for overall type of the expression, but of course they do matter for codegen doing branching and value unwrapping.
The downside of this ruleset are the intermediate allocations for Some x.
But as soon as one would want to heuristically solve it to keep allocations at a minimum (e.g. maintaining T|null if the RHS supports null, or selectively balancing between T|null and Nullable<T> where possible), the ruleset would get more complicated and less predictable (and likely less refactoring friendly). And out of the options presented, option is the most F#-native container type for missing values, with established functions to work with it.
Open question: If .D, i.e. the last RHS in a chain A?.B?.C?.D, is itself a property with missing-value-container type, should it be flattened? What if the containers are not compatible, which one should win ( LHS / RHS / always option / always voption / .. )?
Nullable<T>, T|null maps to option<T> The downside of this ruleset are the intermediate allocations for Some x.
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.
Open question: If .D, i.e. the last RHS in a chain A?.B?.C?.D, is itself a property with missing-value-container type, should it be flattened? What if the containers are not compatible, which one should win ( LHS / RHS / always option / always voption / .. )?
I would expect that
- No, it shouldn't be flattened, but there should be a way to easily make it flattened (like adding one more ?. in the end)
- Left-most container should be used (I hope this is what LHS stands for), e.g.
AinA?.B?.C?.D
Open question: If .D, i.e. the last RHS in a chain A?.B?.C?.D, is itself a property with missing-value-container type, should it be flattened? What if the containers are not compatible, which one should win ( LHS / RHS / always option / always voption / .. )?
I would expect that
- Yes, it should be flattened
We will likely not special-case it, as discussed before. We already do not in all other cases.
We will likely not special-case it, as discussed before. We already do not in all other cases.
Yes, sorry, changed the message
We will likely not special-case it, as discussed before. We already do not in all other cases.
Yes, sorry, changed the message
Yes, easier flattening is something we should consider.
It would be interesting to enable this operator for any types that followed a pattern of intrinsic or augmented HasValue, GetValueOrDefault, and op_Implicit members, as in https://github.com/fsharp/fslang-suggestions/issues/14#issuecomment-284818519.
Such a pattern could cover multiple "optional" types ('T option, 'T voption, Nullable<'T>) and pseudo-types ('T | null) and enable target-typing of the result.[^1]
Here's a runnable example that uses SRTPs and custom operators to illustrate the idea.
(Note that the custom operators are meant to stand in for ?., and I'm only using SRTPs to showcase the pattern; I'm not actually proposing that ?. be implemented using SRTPs, or that an Optional<'Option, 'T> type actually be exposed. It is more likely that the compiler would simply search for and use the appropriate members internally, as it already does for other constructs like for … in … do …, expr[…], etc., etc.)
open System
#nowarn "61"
type Optional<'Option, 'T
when 'Option : (member HasValue : bool)
and 'Option : (member GetValueOrDefault : unit -> 'T)
and ('Option or 'T) : (static member op_Implicit : 'T -> 'Option)> = 'Option
/// Map.
let inline (<&>) (x : Optional<'Option1, 'T>) ([<InlineIfLambda>] f : 'T -> 'U) : Optional<'Option2, 'U> =
if x.HasValue then ((^Option2 or ^U) : (static member op_Implicit : 'U -> 'Option2) (f (x.GetValueOrDefault ())))
else Unchecked.defaultof<'Option2>
/// Bind.
let inline (>>=) (x : Optional<'Option1, 'T>) ([<InlineIfLambda>] f : 'T -> 'Option2) : Optional<'Option2, 'U> =
if x.HasValue then f (x.GetValueOrDefault ())
else Unchecked.defaultof<'Option2>
//
// Example with custom option type.
//
[<CompilationRepresentation(CompilationRepresentationFlags.UseNullAsTrueValue)>]
type Option<'T> =
| Some of 'T
| None
member this.HasValue = match this with Some _ -> true | _ -> false
member this.GetValueOrDefault () = match this with Some x -> x | None -> Unchecked.defaultof<'T>
static member op_Implicit x = Some x
and 'T option = Option<'T>
type A = { B : B option }
and B = { C : C option }
and C = { D : int }
let a = Some { B = Some { C = Some { D = 3 } } }
let x : int option = a >>= _.B >>= _.C <&> _.D
//
// Example with System.Nullable<'T>.
//
type [<Struct>] E = { F : Nullable<F>}
and [<Struct>] F = { G : Nullable<G> }
and [<Struct>] G = { H : int }
let e = Nullable { F = Nullable { G = Nullable { H = 4 } } }
let y : Nullable<int> = e >>= _.F >>= _.G <&> _.H
//
// Example with mixed optional types.
//
type [<Struct>] X = { Y : Nullable<Y> }
and [<Struct>] Y = { Z : int }
let foo = Some { Y = Nullable { Z = 99 } }
let bar : int option = foo >>= _.Y <&> _.Z
let baz : Nullable<int> = foo >>= _.Y <&> _.Z
let qux : Nullable<Y> = foo >>= _.Y
If Optional<'Option, 'T> were to be defined in FSharp.Core, then it could add and default 'Option : 'T option to enable target-typing while not requiring annotation in the absence of other type information:
type Optional<'Option, 'T
when 'Option : (member HasValue : bool)
and 'Option : (member GetValueOrDefault : unit -> 'T)
and ('Option or 'T) : (static member op_Implicit : 'T -> 'Option)
and default 'Option : 'T option> = 'Option
[^1]: I guess for nullable reference types there could be an implicit augmentation along the lines of this:
```fsharp
type 'T when 'T : null with
member this.HasValue = match this with null -> false | _ -> true
member this.GetValueOrDefault () : 'T | null = this
static member op_Implicit (x : 'T) : 'T | null = x
```
In C#'s elvis operator, flattening is not a concern - ((string?)?)? would not make any sense, it naturally just keeps a single level of a possibly missing value.
The inclusion of voption/option change that, as well as the possibility of combining multiple container types in a single long identifier.
Pragmatically, since the purpose of the feature is to avoid nested pattern matching for short one-liners, I think there is a case for always flattening. Which would then need either a default container for the result, or a ruleset for deciding the right container.
module rec Test =
type MyRecord = { Opt : MyRecord option; Vopt : MyRecord voption; MaybeNull : MyRecord | null; JustInt: int}
let process (x:MyRecord) = x.MaybeNull?.Opt?.Vopt?.JustInt // degenerate case to illustrate the need for a ruleset
let moreLikelyCase(x:MyRecord) = x.MaybeNull?.MaybeNull?.MaybeNull?.MaybeNull?.JustInt
// Expected for DTOs coming from serializers etc. I think here it is clear to desire flattening, i.e. returning `option<int>`
We will likely not special-case it, as discussed before. We already do not in all other cases.
Yes, sorry, changed the message
Yes, easier flattening is something we should consider.
Explicit flattening of the very last value would be semantically equivalent to something like x?.id (id meaning identity).
But I am not sure if adding even more changes to the language would help here.
...for any types that followed a pattern of intrinsic or augmented...
I would also favor a more universal approach, since I have seen codebases with domain specific option-like types.
Nevertheless, I think the design should then think about composability of these types if different container types are used within the same expression separated with .?.
Maybe in practice this is a problem just for Nullable / T | null because of their 'T constraints not allowing arbitrary 'T->'U transformation. In that case, the ruleset could be specialized for them and all unconstrained containers would behave uniformly.
I'd also like to mention, that ?. shouldn't come alone, if it is to be implemented, there should be ?? operator as well. This case is very common (using external interop C# object and converting it into non-nullable value to be used later on):
let x: string = A?.B?.C?.D ?? ""
@Lanayx
I'd also like to mention, that
?.shouldn't come alone, if it is to be implemented, there should be??operator as well. This case is very common (using external interop C# object and converting it into non-nullable value to be used later on):
let x: string = A?.B?.C?.D ?? ""
We don't plan introducing it. Option.defaultValue should be used instead. If users want, they can introduce their own operator.
If users want, they can introduce their own operator.
It won't be lazy on the right hand side though 🫤