Open baronfel opened 7 years ago
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:
@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:
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 🙂.
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
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|null maps to option 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
1) No, it shouldn't be flattened, but there should be a way to easily make it flattened (like adding one more ?. in the end)
2) Left-most container should be used (I hope this is what LHS stands for), e.g. A
in A?.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
1) 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 🫤
If users want, they can introduce their own operator.
It won't be lazy on the right hand side though 🫤
I don't understand what that means. That it will be executed every time? It doesn't matter much, it will be inlined most of the times, and JIT optimised, so it won't cost almost anything. ?. won't be something new to language, it will just be a short circuit returning option.
Here's my perspective:
https://github.com/ken-okabe/vanfs/blob/main/README-whatisNull.md https://github.com/ken-okabe/vanfs?tab=readme-ov-file#nullable
We need Nullable Types(nullable reference types).
Thus, given the presence of Nullable Types, it is logical to introduce a Nullable operator (?. ) within the mathematical framework. It's not a matter of the comparison to C# or JavaScript/TypeScript etc., who have simply fulfilled their obligation to maintain consistency within their algebraic structures by incorporating the Nullable operator—a natural and necessary step given the presence of Nullable Types.
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.
As detailed in https://github.com/ken-okabe/vanfs/blob/main/README-whatisNull.md,
Nullable types are, by definition, incapable of containing nested structures. Therefore, since the set(operand) targeted by the Nullable operation (?. ) does not contain nested structures, there is no need for discussions on whether or not to flatten it.
Simply put, if the operand paired with the Nullable operator (?. ) is not a Nullable Type, it's a type mismatch and the compiler will raise an error.
If users want, they can introduce their own operator.
It won't be lazy on the right hand side though 🫤
I don't understand what that means. That it will be executed every time? It doesn't matter much, it will be inlined most of the times, and JIT optimised, so it won't cost almost anything. ?. won't be something new to language, it will just be a short circuit returning option.
It means for example that ?? { x = 1 }
will always allocate, and ?? failwith "oh no"
will always throw. Ie situations where I would currently use Option.defaultWith
instead of Option.defaultValue
.
If users want, they can introduce their own operator.
It won't be lazy on the right hand side though 🫤
I don't understand what that means. That it will be executed every time? It doesn't matter much, it will be inlined most of the times, and JIT optimised, so it won't cost almost anything. ?. won't be something new to language, it will just be a short circuit returning option.
It means for example that
?? { x = 1 }
will always allocate, and?? failwith "oh no"
will always throw. Ie situations where I would currently useOption.defaultWith
instead ofOption.defaultValue
.
Yeah, that what I would suggest - use defaultWith. We're not too keen on the idea of adding a new language construct which won't be an expression, but a special "operator" (??) which will expand into piping + generating and implicit lambda.
Yeah, that what I would suggest - use defaultWith
Do you mean adding defaultWith
function to Fshap.Core? Asking, since currently it only exists for Option and ValueOption, for Nullable and NRT this doesn't exist
@Lanayx :
The nullness RFC impl is adding inlined defaultIfNull
.
defaultWithIfNull
taking in a lambda it's not there yet, but can be added - if that is what you meant.
there is no need for discussions on whether or not to flatten it.
@ken-okabe : That is only true if the proposal covered only NRTs. However, since:
'T
due to their generic constraintsThe flattening question is real and affects design choices.
The nullness RFC impl is adding inlined
defaultIfNull
.defaultWithIfNull
taking in a lambda it's not there yet, but can be added - if that is what you meant.
Yes, this is what I meant, it's clearly missing to avoid allocation (and/or side effects) in this case
// current PR
let x: MyType = y?.A?.B |> defaultIfNull (MyType())
// should exist
let x: MyType = y?.A?.B |> defaultWithIfNull (fun () -> MyType())
However, there are multiple problems with "many new functions" approach 1) It becomes very verbose, and lambda shorthands are not applicable here. Compare this with
// suggesting
let x: MyType = y?.A?.B ?? MyType()
2) It's not uniform across types (doesn't look nice to me):
let x1 = None |> Option.defaultWith 1
let x2 = ValueNone |> ValueOption.defaultWith 1
let x3 = Nullable() |> defaultIfNullV 1
let x4 = y?.Value |> defaultIfNull 1
In that case it would make more sense to deprecate defaultArg
and defaultValueArg
functions (as well as specialized module functions) and introduce new default
and defaultWith
functions (probably using some overloads and/or SRTP magic), that will work for all 4 types (and maybe even future types).
At the same time ??
operator could actually cover all those cases above + potentially merge two cases into one, like if it's just value, then use default
, if it's some invocation (property, method, costructor) then use defaultWith
. This options seems to be the most attractive to me and will be a companion to ?.
operator.
P.S. And we shouldn't forget about !.
operator as well, people might want to ignore null checks in certain cases
@T-Gro Given that, the suggestion itself sounds inappropriate to me.
A single operator serving multiple types can lead to confusion for both the compiler and the programmer. The nullable operator is specifically designed for nullable types, and if a new operator for option type are required, a separate, dedicated optional operator should be used.
Why do we want to introduce unnecessary complexities?
Yeah, that what I would suggest - use defaultWith
Do you mean adding
defaultWith
function to Fshap.Core? Asking, since currently it only exists for Option and ValueOption, for Nullable and NRT this doesn't existlet x4 = y?.Value |> defaultIfNull 1
No null propagation will always return an option (or value option, depending how we will design it), and we already have defaultwith and defaultvalue.
// suggesting let x: MyType = y?.A?.B ?? MyType()
We will for sure not be introducing a new construct (which can't be classic operator, because it needs to transform rhs to lambda) to the language.
I think, there's a slight confusion of what @T-Gro meant. What we discussed internally for the value propagation syntax (?.
) is that for all the "null-like" and "option-like" types (being 'T | null
, System.Nullable<'T>
, 'T option
and 'T voption
) we will have a syntax for accessing its wrapped type safely, which will always covert to an option or a value option (depending on our design decisions), for example (in semi-pseudo example code to get the idea):
type Foo() =
member _.ReturnsOption = if System.Random.NextInt() % 2 = 0 then Some("foo") else None
member _.ReturnsVOption = if System.Random.NextInt() % 2 = 0 then ValueSome("foo") else ValueNone
member _.ReturnsNullable = if System.Random.NextInt() % 2 = 0 then "foo" else null
member _.ReturnsSystemNullalbe = if System.Random.NextInt() % 2 = 0 then Nulalble(123) else Nullable(Unchecked.defaultof<_>)
let foo = Foo()
foo?.ReturnsOption?.Length // Option<int>
foo?.ReturnsVOption?.Length // Option<int>
foo?.ReturnsNullable?.Length // Option<int>
foo?.ReturnsSystemNullable?.Length // Option<int>
defaultWith
or defaultValue
later.cc @dsyme
Okay this clarifies things. I'm not sure I fully agree though. Uniformity is nice, but I think option
mapping to voption
or vice-versa would be very surprising for the user.
Okay this clarifies things. I'm not sure I fully agree though. Uniformity is nice, but I think
option
mapping tovoption
or vice-versa would be very surprising for the user.
This is not meant to replace normal options or value options handling.
It's meant for propagation in the method chain calls (or chaining properties), which will highly likely be nullrefs in vast majority of cases. We don't want complex mapping rules when user can't expect what would be a result of long chaining and will need a decoder ring to figure out whether it will return option, voption or one of two nullables.
Like,
dto.Order?.Customer?.Address?.Street?.Name
will always have a known type at all points when it might short circuit no matter how dto is designed (maybe some parts are options, some voption and other nullables).
I agree with @Tarmil, currently almost all Fsharp.Core is monadic, i.e. operators and functions returns the same type. If we map everything to Option
, then it's almost a must to do flattening, because there might be case for Option<Nullable<int>>
and this
let x = A?.MyNullableProp |> Option.defaultValue |> defaultIfNullV 1
looks horrible.
I agree with @Tarmil, currently almost all Fsharp.Core is monadic, i.e. operators and functions returns the same type. If we map everything to
Option
, then it's almost a must to do flattening, because there might be case forOption<Nullable<int>>
and thislet x = A?.MyNullableProp |> Option.defaultValue |> defaultIfNullV 1
looks horrible.
It won't be. Nullable will be converted to option, it's clearly seen from the examples above. But if your property itself is int option option
or Option<Nullable<int>>
, it won't be flattened, only outermost container will be processed.
E.g. if your property is member _.Foo = Some(Nullable(3))
, then we will only generate the check for Some, and not for underlying Nullable.
You can think of ?.
as if it was pining to Option.ofObj
of sorts.
It won't be. Nullable will be converted to option, it's clearly seen from the examples above
But you didn't show such examples above? The example will be
type Foo() =
member _.A = Nullable(1)
let x = Foo()
let y = x?.A // Option<Nullable<1>>
From our previous conversation I understood that "we won't be ourselves flatten nested (v)options in sake of uniformity and not having special cases"
It won't be. Nullable will be converted to option, it's clearly seen from the examples above
But you didn't show such examples above? The example will be
I 100% did, just with some underlying property. Without property it will generate HasValue check and will wrap the value in option, not Nullable itself. Let me change it so it's more clear
foo?.ReturnsSystemNullable?.Length // Option
type Foo() = member _.A = Nullable(1) let x = Foo() let y = x?.A // Option<Nullable<1>>
From our previous conversation I understood that "we won't be ourselves flatten nested (v)options in sake of uniformity and not having special cases"
Yes, if you decide to return option<option<_>>, it's on you.
There's a lot confusion for it. I think next step is that we need an rfc for it listing all the corner cases and what would be produced for each.
Some things to cover in rfc
I 100% did, just with some underlying property. Without property it will generate HasValue check and will wrap the value in option, not Nullable itself. Let me change it so it's more clear
Sorry, I still don't understand from this explanation, what would you like the type of y
be in my case (not a rare case from consuming C# classes)
type Foo() =
member _.A = Nullable(1)
let x: Foo | null = Foo()
let y = x?.A
should it be Option<Nullable<int>>
or Option<int>
? First means no flattening, second means implicit flattening. In first case we'll have to use y |> Option.defaultValue |> defaultIfNullV 1
to get default value
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