I want to propose a new syntax for optional accessing:
FooStruct?.bar
FooArray?[10]
FooDict?["bar"]
FooStruct?.bar
for optional accessing of struct
fields. If the field is not present in the struct, the result will be missing
instead of an error. The return will be Foo.bar
if bar
exists in Foo
(normal case).
FooArray?[10]
for optional accessing of Array
elements. If the length of the given number is more than the length of the Array (out of bounds), the result will be missing
instead of an error and will be Foo[10]
in normal case.
FooDict?["bar"]
for optional accessing of Dict
values.
If the key does not exist in the Dict
, the result will be missing
instead of an error and will be Foo["bar"]
in normal case.
The implementation is straight forward. For example, for the Dict
:
if haskey(FooDict, "bar")
return Foo["bar"]
else
return missing
end
Instead of missing
, we can return nothing
too. I prefer missing
here.
Isn't this pretty much the same as get
?
julia> a = rand(5);
julia> get(a, 6, missing)
missing
julia> get(a, 4, missing)
0.03053521360980338
julia> d = Dict("foo" => "bar");
julia> get(d, "baz", missing)
missing
julia> get(d, "foo", missing)
"bar"
Isn't this pretty much the same as
get
?
For Arrays and Dicts yes. For structs, the method doesn't exist though.
This is a simple extension to []
and .
, which I find quite useful and intuitive. It allows us to still use the nice []
and .
, while have more functionality.
Things like get.(Ref(A), 1:2, missing)
could be just written as A?[1:2]
For structs, the method doesn't exist though.
That is true but you quite rarely access fields (properties) using variables in non-metaprogramming code.
Things like get.(Ref(A), 1:2, missing) could be just written as A?[1:2]
Oh, so it should also implicitly broadcast? https://github.com/JuliaLang/julia/issues/19169 might be a bit relevant then.
I think another proposal for ?
that has come up previously is for foo?(x)
which would mean something like x === nothing ? x : f(x)
(or missing
) as a shorthand lifting syntax.
For structs, the method doesn't exist though.
That is true but you quite rarely access fields (properties) using variables in non-metaprogramming code.
This is very useful in certain applications like XML, HTML. For example, I might want to access a deep property.
city?.university?.people?.name
Writing this manually produces a mess:
if hasfield(city, :university)
if hasfield(city.university, :people)
if hasfield(city.university.people, :name)
return city.university.people.name
else
return missing
end
else
return missing
end
else
return missing
end
Things can be mixed too:
city?[10]?.university?.people?["Tom"]?.age
Things like get.(Ref(A), 1:2, missing) could be just written as A?[1:2]
Oh, so it should also implicitly broadcast? #19169 might be a bit relevant then.
I think that is the natural way. For example, for A[1:2]
we don't write A.[1:2]
. However, whatever decision is made for that one, should be used for this one too.
I think another proposal for
?
that has come up previously is forfoo?(x)
which would mean something likex === nothing ? x : f(x)
(ormissing
) as a shorthand lifting syntax.
That it is limited to one argument functions, and I don't quite like that. ?
should check or refer to a feature of its left hand side, not something on the right hand side.
Instead, we can say ?|>
means what you want:
x ?|> foo
```jl
x === nothing ? missing : foo(x)
That is quite in line with this issue. Since we are using `?` for something on its left hand side. It also solves the issue with one-argument functions.
The meaning for (I don't know if this is useful though)
```jl
foo?(args...)
could be:
if isa(getfield(@__MODULE__, :foo), Function)
return foo(args...)
else
return missing # or something else
end
Here we interpreted ?()
as the existence of the function.
Anyways, optional accessing doesn't interfere with foo?(x)
or x ?|> foo
.
You could do something like this for that use case:
julia> struct A
a
end
julia> struct MyMissing end
julia> const mymissing = MyMissing()
MyMissing()
julia> Base.getproperty(a::A, s::Symbol) = hasfield(A, s) ? getfield(a, s) : mymissing
julia> Base.getproperty(::MyMissing, s::Symbol) = mymissing
julia> a = A(1)
A(1)
julia> a.a
1
julia> a.b
MyMissing()
julia> a.b.c.d.e
MyMissing()
But then you do need to use a custom missing type (MyMissing
here) if you don't want to pirate and you can't opt into it at the callsite (but maybe for some kind of XML type you would always want this behavior).
@ericphanson Thank you for the solution, however, this:
Base.getproperty
, which is not what I want all the time. Sometimes I just want Julia to return an error for me.a.b.?c
, I can say that c
might be optional and return an error if b
does not exist.If this is to be done, then nothing
is the right sentinel to use, not missing
.
If this is to be done, then
nothing
is the right sentinel to use, notmissing
.
To be honest, I prefer to have a new type similar to missing
, but with access propagation. I mean something like:
# the new missing:
missing.foo == missing
missing[1] == missing
missing["a"] == missing
This allows partial parsing of the sentence.
For example, the following can be parsed partially because missing
will propagate until the end, and the result will be missing:
city?.university?.people?.name
# if city didn't have university, the end result will be missing, while still being parsed partially
But if we don't have access propagation, we need to parse the whole thing in a chunk.
_Since TypeScript and JavaScript 2020 have a similar syntax, I wonder how they have implemented this._
My other reasons for preferring missing like type:
nothing
(if not taken care of) will soon throw an error. But missing
propagates.missing
allows _loose_ usage of ?
. Remember ?
is not about being exact here. We just want to relax and call a deep property. missing
still allows constrained usage with checking with ismissing()
right after the evaluation.The missing
singleton is expressly about representing missing _data_ whereas nothing
is used to represent structural absence of a value of interest. In other words, missing
is something that can come from the data whereas nothing
is something that can come from the API to indicate that there wasn't any value to return. I'm not sure how you got it turned around but every example that you've presented should use nothing
rather than missing
. There are literally no APIs in Base or stdlib that return missing
to indicate the absence off something, whereas there are several, like the find*
functions, which use nothing
to indicate that there was nothing to be found. And there's no way we're introducing a third new kind of absence of value.
Since TypeScript and JavaScript 2020 have a similar syntax, I wonder how they have implemented this.
You define the x?.y
and x?[y]
operators to mean
x === nothing && hasproperty(x, :y) ? getproperty(x, :y) : nothing
x === nothing && haskey(x, y) ? getindex(x, y) : nothing
That's also why there's no need to cram any weird behaviors into the nothing
value, you can just define the .?
and .[]
appropriately to return nothing
when x
is nothing
.
Might want to return a Some
wrapped value here though.
True, that would be more general and allow handling the case where x.y
exists and has the value nothing
. Not entirely clear if that's desirable/necessary.
Seems pretty common to have nothing
as field values (as opposed to keys).
That is true, but it seems likely that one would want x?.y
to return nothing
if either x.y
exists and is nothing
or if x.y
has no y
property at all (and maybe even if it's #undef).
:+1: for using Some
here. If we use Some
, there is a nice extension for this to work on the left hand side. For example,
dict?[:x] = obj?.x
would mean to
obj.x
does not exist (i.e., obj?.x === nothing
), remove key :x
from dict
.obj.x
exists (i.e., obj?.x isa Some
), put it in dict[:x]
, even if it is nothing
.Similar trick can work for "flexible" object type with a variable set of properties.
Ref:
https://github.com/jw3126/Setfield.jl/issues/65
https://github.com/JuliaLang/julia/pull/33758
Just to be explicit, with the Some
approach, the result of obj?.x
would be nothing
when obj.x
does not exist but it would be Some(obj.x)
when it does, which I suspect is not what @aminya wants when writing obj?.x
to get the value of obj.x
. I.e. obj?.x
and obj?[x]
would only ever return the value nothing
or Some(obj.x)
or Some(obj[x])
.
Most helpful comment
Might want to return a
Some
wrapped value here though.