-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Make responds_to?
match all arguments
#2549
Comments
I would very much like to still see a clean way of just checking for the method name, since in many cases that will probably suffice, and the code will then be less cluttered. Possibly as another pseudo-method? |
Or the same name but like |
I would prefer being able to specify a type instead of actual values. So for example instead of the example you made
|
I was actually bitten by this a few times. In my particular case, I need to compute several possible values for that method, so I think will be better if I can match the method signature's types instead. That way I avoid having the pre-calculated values for the method signature check when the compiler have already discarded that code block. |
As stated in #1551, |
I think having class Bar
def foo(x)
end
def bar(x : Int32)
end
def baz(x : Int32, y : String)
end
end
bar = Bar.new
bar.responds_to?(:foo) # => true
bar.responds_to?(foo()) # => false
bar.responds_to?(bar(Object)) # => true
bar.responds_to?(bar(Int32)) # => true
bar.responds_to?(bar(String)) # => false
bar.responds_to?(baz(Int32, y: String)) # => true
bar.responds_to?(baz(x: Int32, y: String)) # => true So if Maybe in some cases you already have a variable that you'll pass to the method so it's easier to check it with the type of that: x = 1
bar.responds_to?(bar(typeof(x))) # => true What do you think? I can try to implement it. |
Wouldn't bar.responds_to?(:foo) # => true
bar.responds_to?(:foo, nil) # => false # Maybe use something special like Void or NoReturn
bar.responds_to?(:bar, Object) # => true
bar.responds_to?(:baz, Int32, y: String) # => true syntax be better? Otherwise I feel like calling |
@asterite I like your proposal, the only thing that's not clear enough i think when I read One idea to make it more obvious that |
@bew I really like that idea. I wanted to make it look like a call because that's what you are testing, whether it responds to a given call, even specifying a block (I didn't think of a syntax for that yet). The dot makes it clearer. |
|
How do you distinguish between "responds to a method regardless of the arguments" and "responds to a method with the given arguments" in your proposal? |
responds_to?(:foo)
responds_to?(:foo, Int32, String)
responds_to?(:foo, Int32, y: String)
responds_to?(:foo, x: Int32, y: String) The only issue is with "responds to a method explicitly without arguments". I'm thinking of responds_to?(:foo, nil) or responds_to?(:foo, Void) |
Well, that special case right there is why I prefer my proposal: if it's a symbol then it's the first form, if it's a call then it's the second form. It's better to always try to avoid special cases. |
Regarding type arguments, I think it can easily become very cluttered, when you have union types and either need to declare them explicitly or wrap them in Maybe we could make if foo.responds_to?(.bar(x))
foo.bar(x)
end Otherwise we might consider to implement that specific use case in a method |
@straight-shoota That would break BC and lot of code out there. |
The only problem with passing variables like that is when the argument is an actual type (like one you would pass to a |
@Sija True. The question is, will this variant still be used a lot or more and more replaced by explicitly typed @asterite Yes, that's a downside because it can lead to slightly unexpected behaviour. But it might be excusable for avoiding tons of |
Is this the goal? Symbol syntax would need to stay for autocasting enum arguments and may be implementing compile-time checks in Symbols is what everyone in Crystal and Ruby got used to. It feels immutable and baked into the code. It matches perfectly with defined method existence check. Dot syntax looks awful. Having an orphan dot (
False. Edge cases are everywhere. Everything should be as simple as it can be, but not simpler -- over-abstraction is bad. And in this particular case expressiveness and beauty are at risk. |
@vladfaust I'm only referring to symbol literals used with The leading dot syntax is similar to what we already have with |
Maybe we should just do away with the "method call" appearance of this keyword now and do something like if object responds_to? some_method(String) so akin to for example Java's if callable?(object.some_method(some_string)) |
How about using block argument prefix? foo.responds_to?(&.bar(1, :dos, "tres")) |
My point is that this doesn't align with any of the semantics the proposed constructs currently have. These points have largely been made, but to summarize:
That's why I'm proposing to either introduce an entirely new syntax construct that doesn't really exist like this in the grammar yet, or at least fit it into the existing duality we already have in the call syntax. That is I'm not a big fan of this duality existing in the first place (and one could argue we already have this duality in another place, through * I prefer reusing the |
With all the suggested syntaxes, you can't check if a prefix calls like Also I think that the prefix dot-notation cannot be used with a parentheses-less Another suggestion: What if we do: obj.responds_to?(~_)
obj.responds_to?(_.foo)
obj.responds_to?(_ + 2) # maybe not this...
obj.responds_to? _.write(Slice)
obj.responds_to? _.bar(*) Where Since it's known that in Crystal |
My suggestion solves this easily and naturally, without any new concept to learn: if callable?(~obj) |
@bew Prefix operators are just normal methods without arguments. Using |
@jhass oh right, I didn't see it in your summary post, that's interesting too! |
With the |
The only thing I don't like with if callable?(obj.foo)
obj.foo
end Other option, a special method / keyword: (note: naming is hard) do_if_compiles? do
obj.foo
end |
|
In most use cases you have a particularly call that is optional (i.e. only call if it's a valid call). E.g. Is there any benefit of specifying Per arguments in #2549 (comment) I agree that a top-level method-like syntax is better than the current Block arguments haven't been mentioned yet at all, and I suppose they would be particularly difficult with type arguments. But As suggested in #2549 (comment) it seems very reasonable to have a generic Generalization could even go a step further: This is very much like a nilable Because typical use cases revolve around checking if a call is possible and then making that call happen, there could be a convenient solution for that. if compiles?(a.b(c))
a.b(c)
end Nobody likes to write macro try(call)
if compiles?({{ call }})
{{ call }}
end
end
try a.b(c) I'm using |
Hm, I guess with a more generalized implementation it would be quite complex to apply filters to guard the actual code. |
The problem with a general x = 1 || 'a'
y = 1 || 'a'
if compiles?(x < y)
# (x.is_a?(Int32) && y.is_a?(Int32)) || (x.is_a?(Char) && y.is_a?(Char))
end The equivalent x = 1 || 'a'
y = 1 || 'a'
if x.responds_to?(&.<(y))
# neither `Int32#<(Int32 | Char)` nor `Char#<(Int32 | Char)`
# compiles, so this branch is unreachable
end To me this seems more reasonable because the filter can only act on its receiver ( There is also a difference between overload matching and successful compilation, because the former does not actually have to type the method bodies and the latter must: (..).responds_to?(&.each) # => true
compiles?((..).each) # => false A minor note is that some type names can be used even if the augmented x = uninitialized Int32
1.responds_to?(&.+(x)) # => true |
To add a bit more to the discussion, instead of |
Something not mentioned is that passing arguments themselves instead of their types means autocasting applies to a bigger set of expressions: enum Bar
X
end
class Foo
def foo(x : Int16); end
def bar(x : Bar); end
end
x = Foo.new
v = 0_i8
s = :x
# arguments
x.responds_to?(&.foo(0)) # => true
x.responds_to?(&.foo(0_i8)) # => true
x.responds_to?(&.foo(1234567)) # => false
x.responds_to?(&.foo(v)) # depends on `-Dno_number_autocast`
x.responds_to?(&.bar(:x)) # => true
x.responds_to?(&.bar(s)) # => false
# argument types
x.responds_to?(&.foo(Int8)) # depends on `-Dno_number_autocast`
x.responds_to?(&.foo(v)) # not allowed
x.responds_to?(&.bar(Symbol)) # => false
x.responds_to?(&.bar(:x)) # not allowed
x.responds_to?(&.bar(s)) # not allowed |
Just to be clear: Shouldn't That said, I would prefer value based argument matching anyways. |
Fixed and added examples for enums (which only work on literals). However, general Also, to clarify, all the calls of the second kind are also valid calls of the first kind, passing metaclasses and looking for methods that take e.g. an |
Consider this code:
responds_to?
, in its current form, is pretty weak. It only allows checking whether an object responds to a method with a name, but doesn't consider the arguments or its types.We should maybe make
responds_to?
receive, in addition to the method name, the arguments and even a block. With that, we could check if a method can be invoked with those arguments and block, before actually performing the call (which will now never give a compile error).This will also cover #2391, or at least part of it, because you could check that a method doesn't respond to a method with a given set of arguments (for example, I can test that a method should accept a string argument but not an int).
With this change, a call to
.responds_to?(:foo)
will check that there's an argless methodfoo
. We should maybe have a way to check that there's any methodfoo
, regardless of the arity, though I'm not sure it would be very useful (you usually useresponds_to?
before invoking a method, and you always have the arguments to it).Thoughts?
The text was updated successfully, but these errors were encountered: