Julia: using `?.` and `?[ ]` for optional accessing [syntax]

Created on 2 May 2020  路  16Comments  路  Source: JuliaLang/julia

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.

feature speculative

Most helpful comment

Might want to return a Some wrapped value here though.

All 16 comments

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 for foo?(x) which would mean something like x === nothing ? x : f(x) (or missing) 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:

  • requires overriding Base.getproperty, which is not what I want all the time. Sometimes I just want Julia to return an error for me.
  • In 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, not missing.

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

  • If obj.x does not exist (i.e., obj?.x === nothing), remove key :x from dict.
  • If 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]).

Was this page helpful?
0 / 5 - 0 ratings