Skip to content
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

using ?. and ?[ ] for optional accessing [syntax] #35699

Open
aminya opened this issue May 2, 2020 · 16 comments
Open

using ?. and ?[ ] for optional accessing [syntax] #35699

aminya opened this issue May 2, 2020 · 16 comments
Labels
feature Indicates new feature / enhancement requests speculative Whether the change will be implemented is speculative

Comments

@aminya
Copy link

aminya commented May 2, 2020

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.

@KristofferC
Copy link
Member

KristofferC commented May 2, 2020

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"

@aminya
Copy link
Author

aminya commented May 2, 2020

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]

@KristofferC
Copy link
Member

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? #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.

@aminya
Copy link
Author

aminya commented May 2, 2020

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
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)

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.

@ericphanson
Copy link
Contributor

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).

@aminya
Copy link
Author

aminya commented May 2, 2020

@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.

@StefanKarpinski
Copy link
Member

If this is to be done, then nothing is the right sentinel to use, not missing.

@StefanKarpinski StefanKarpinski added feature Indicates new feature / enhancement requests speculative Whether the change will be implemented is speculative labels May 4, 2020
@aminya
Copy link
Author

aminya commented May 4, 2020

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.

@StefanKarpinski
Copy link
Member

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.

@StefanKarpinski
Copy link
Member

StefanKarpinski commented May 4, 2020

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.

@KristofferC
Copy link
Member

Might want to return a Some wrapped value here though.

@StefanKarpinski
Copy link
Member

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.

@KristofferC
Copy link
Member

Seems pretty common to have nothing as field values (as opposed to keys).

@StefanKarpinski
Copy link
Member

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).

@tkf
Copy link
Member

tkf commented May 5, 2020

👍 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:
jw3126/Setfield.jl#65
#33758

@StefanKarpinski
Copy link
Member

StefanKarpinski commented May 5, 2020

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]).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature Indicates new feature / enhancement requests speculative Whether the change will be implemented is speculative
Projects
None yet
Development

No branches or pull requests

5 participants