-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
RFC: remove invoke
#13123
RFC: remove invoke
#13123
Conversation
+1 |
I've never liked |
This seems reasonable: For future reference: we can then also close #7045. |
How would I do this after this PR: function Base.show{T<:MyType}(io::IO, a::Type{T})
println("This is one of my types:")
invoke(show, Tuple{IO, DataType}, io, a)
end |
@mauro3 I don't have a perfect answer, but there are a couple options:
|
It certainly is a common OO pattern to build upon the methods which are defined for the supertype which is what I do in that example. Maybe something like this could support that:
|
I don't see how |
I guess |
That was a bit a stupid comment of mine as my proposition really is the same thing as invoke but different syntax. Anyway, this could be used instead of invoke:
and may could be @StefanKarpinski's escape hatch? Is there any difference to invoke? |
That isn't type safe; nothing will check that |
Thanks for the clarification! I vote for keeping |
If we're going to keep it, some interface like |
The syntax I used for this was |
i was pointed here after asking for the equivalent of calling a super-method. i would appreciated a call-next-method or similar. i honestly don't know why this idiom is not common in julia. guesses include: julia being largely numerical computing (so many types are simple values); multiple dispatch helping decompose "gnarly" complex types into simpler constituents; lack of libraries that rely on inheritance because only leaf types are concrete. |
We just spent all day looking for a workaround for the obvious problem of how to call a more general implementation from within a more specific implementation of a function (in particular across packages/modules), and everything we tried had either been removed from Julia or doesn't work. We were finally happy when we discovered invoke, and now that too is being removed! Why would you take something like this out of the language? Please, by all means, mark it "this is very slow and should not be used unless absolutely necessary" in the documentation. But please, please reconsider removing things from the language before an equally capable replacement has been developed. Without invoke, we don't have any way of doing something that should be trivial. Please see the thread on julia-users that I started earlier today about this. We need this function. |
By the way, the argument Jeff makes about it being an abstraction violation doesn't persuade me. Changing the types for the arguments of a function could also introduce ambiguities that didn't otherwise exist (actually this happens in practice to us). But we don't then remove Julia's sophisticated abstract type system just because this can happen. Besides, if I change the type of the function being called by invoke, the worst that will happen is that the test code (or user code) will break at precisely the point where invoke is used, telling me there is no such function. That seems perfectly reasonable to me. |
Feedback noted! The way this is going, it does not look like we'll end up removing invoke any time soon at least. How do you feel about some of the other options discussed in this thread, e.g. |
Jeff, I am not sure about call_next_method. For what it is worth, the Gap computer algebra system has a mechanism for a function to silently fail and pass to the next most highly ranked method in the method table (users can specify their own personal ranking of methods in GAP). And people swear by it. However, there are also people wanting to move towards a kind of dispatch model based on computed "rules", where a method is called based on the precomputed cost of calling that method vs some alternative. So even that system hasn't worked out perfectly for them. A CAS called Polymake has such a dispatch mechanism. But now people want something that combines both capabilities and computes the best "strategy", based on what has already been computed and cached, so that the cost can change at runtime, etc. (E.g. there is no point in ranking a particular strategy lower because it relies on a high cost determinant computation if that determinant was already previously computed and cached). In other words, the system can never be too flexible for computer algebra. On the other hand, computer algebra systems are unusual, in that they are populated with "types" that depend on runtime values. For example, Z/nZ is a field if and only if n is prime. And Z/nZ depends on the value of n anyway. In such cases we overload call (or () or whatever it is now called) in Julia so that certain objects can behave like types. e.g. for the "type" Z/nZ. There are also other dispatch scenarios we are thinking about, for example calling a special implementation of an algorithm for GCD domains, which are basically integral domains for which a gcd function is available. For this we hope traits will eventually be the solution in Julia. In short, it is not trivial to see what we gain/lose in the long run from any given decision in Julia. We basically see it as a serious research project to push Julia as far as we can given what is available. Mainly we are concerned about existing functionality being taken away after we have come to rely on it, when there is no alternative provided. Believe it or not, there are people seriously trying to rewrite Gap in Julia. And they just might succeed. Performance of all the dynamic clunking around that they need to do for dispatch is not an issue if it is the difference between an algorithm that will see the universe die of heat death before it finishes and one that takes 10 minutes. But the advantage for them is to also benefit from fast Julia dispatch for mid-level algorithms that interpreted computer algebra systems currently can't ever make fast (plus integration with the growing Julia ecosystem). But getting back to your question. I am happier about call_next_method than about removing invoke altogether. I can only comment on how that would affect us right now. I think this would adequately solve the problem we have calling across modules. Basically Nemo is to become a kind of central module containing generic implementations and specialisations provided by various C libraries. On top of Nemo will be many packages, for number theory, group theory, computer algebra, etc., which "plug into" the abstract types provided by Nemo. For free they get the generic implementation in Nemo, if they implement some very basic functionality. But they can then overload this with more specific functionality for their own types. Chances are, the "next_method" after their own implementation fails is the one Nemo provides, and so this should be fine. My concern that remains is actually what happens within a single module, e.g. Nemo itself. The main issue is when over some range a certain implementation is faster than the generic method, but the generic method wins elsewhere (this happens often, and prompted today's difficulties). Some examples of this include classical multiplication of polynomials vs say kronecker segmentation. For sparse polynomials, the classical algorithm will be faster. Otherwise KS will be faster, usually. We currently have the following implemented in our polynomial code:
This is simply because for the types that define * for polynomials over certain rings for which kronecker segmentation is not faster everywhere, we sometimes want to call the classical implementation (which used to just be called *). The only solution was to rename the * function to mul_classical and introduce this rather silly function to call it in the more general case, all so that mul_ks can call mul_classical when it needs to. And then * for those types can first call mul_ks, etc. The function invoke solves this problem for us. The * function for those types can fallback to the more general * method (which implements classical multiplication). But the proposed call_next_method might actually call the wrong version. Technically we could probably work around this by having the "wrong version" also call call_next_method, so that it passes through again to the next most general method, etc., until the right method is finally encountered. This sort of thing actually happens in our matrix code right now (though we solved it with renaming, because we didn't know about invoke). Consider for example the function solve for solving matrix equations Ax = B. There can be implementations of this for rings where coefficient blowup can occur (such as over the integers), and those where it can't (e.g. over Z/nZ), methods based on interpolation when working over polynomial rings and special versions of all these functions for the case where you are working over a field, not a ring. Currently all those cases can be distinguished by (abstract and/or parameterised) type alone in Nemo. But often we have to dispatch to a more general implementation if one method fails. I would be worried that it would be difficult to know which is the next most general method once we get implementations of solve for many more parameterised/abstract types. So sometimes we just need finer control. The less flexible Julia becomes in this regard, the more likely we end up having to introduce functions everywhere whose only job is to look at the types of the arguments at runtime and dispatch to the right version of the function based on their types. This is the situation already with python. In summary, I don't know for sure. I think the proposal would be less flexible for us. But if you decide to do it that way, I think we might be able to live with it, through a combination of renaming and adding additional short functions such as the one I gave above. Sorry for the longwinded answer. I just wanted to illustrate why it's not an easy question to answer definitively. I think we can probably survive if you do go down that route. I will ask my colleagues about it too over the next couple of days, and post a followup if they raise objections I haven't thought of. |
Some related discussion: #17168 (comment) |
In my experience, being able to specifically call a more abstract method is quite valuable. A key use case is when a more specific method is "extending" the functions of the more abstract one. I agree that invoke is maybe too general, an would be in favor of something along the lines of |
I think the current solution, namely, to have f(x::Int, y::Number) = 1
f(x::Number, y::Int) = 2
f(x::Int, y::Int) = callnext(f, (x,y)) # does this call method 1 or 2?
f(x::Int, y::Int) = invoke(f, (Int,Number), x, y) # clearly calls method 1 We could have some syntactic sugar for this in the form of an f(x::Int, y::Int) = @invoke f(x, y::Number) The macro could desugar to something like this: invoke(f, (typeof(x), Number), x, y::Number) I.e. use the actual type of arguments that don't have type annotations and add type asserts to arguments that do have type annotations, as the syntax would suggest (which also would prevent invoking methods that don't apply in the first place). |
that's easy: |
Here's a potential implementation of macro invoke(ex)
ex.head == :call || error("@invoke requires a call expression")
args = ex.args[2:end]
types = map(args) do arg
isa(arg,Expr) && arg.head == :(::) ? esc(arg.args[end]) : :(typeof($(esc(arg))))
end
:(invoke($(esc(ex.args[1])), ($(types...),), $(map(esc,args)...)))
end Used like so: f(::Int, ::Number) = 1
f(::Number, ::Int) = 2
f(::Number, ::Number) = 3
julia> @invoke f(1,2::Number)
1
julia> @invoke f(1::Number,2)
2
julia> @invoke f(1::Number,2::Number)
3
If we remove |
If the |
Note that you need to hoist the evaluation of the arguments and make sure they are evaluated in a intuitive order. The version of |
I'll see if there's interest before trying to fix it. @yuyichao: where's your version of |
I don't see what the macro gets you over using the function. |
Well, the functionality is the same, it's just easier to use. |
It's a bit of a syntax pun on the type assert, to make something that isn't needed all that often slightly more concise. |
To be honest, I've often considered the same sort of thing for @ccall libm.sin(x::Float64)::Float64 |
I was also tempted to comment that I liked the correspondence between |
The types in As far as syntax correspondence goes, |
Also, in a ccall/cfunction you must give the type of every argument. For |
See this SO question: http://stackoverflow.com/questions/39102198/is-it-possible-to-call-an-overloaded-function-from-overwriting-function-in-julia. The fact that someone asking this question very nearly used the exact syntax that I proposed for the |
|
|
I like |
The person asking the question on SO likes |
Here's a version with the macro callsuper(ex)
ex.head == :call || error("@invoke requires a call expression")
args = ex.args[2:end]
types = Symbol[]
vals = Symbol[]
blk = quote end
for arg in args
val = gensym()
typ = gensym()
push!(vals, val)
push!(types, typ)
if isa(arg,Expr) && arg.head == :(::) && length(arg.args) == 2
push!(blk.args, :($typ = $(esc(arg.args[2]))))
push!(blk.args, :($val = $(esc(arg.args[1]))::$typ))
else
push!(blk.args, :($val = $(esc(arg))))
push!(blk.args, :($typ = typeof($val)))
end
end
push!(blk.args, :(invoke($(esc(ex.args[1])), ($(types...),), $(vals...))))
return blk
end This uses gensym a fair bit, maybe emitting a let block with locals would be better. |
Can we incorporate @yuyichao's keyword support: #7045 (comment) |
The kw support was mainly an earlier version of #11165 (combined with some simple ask shuffling to make sure kwcall is correctly handled by the macro). If we want to make This also doesn't have to be in the first version of the macro given that the invoke function doesn't support kwargs yet. |
Let's move the conversation about |
Was pointed to this thread after asking https://discourse.julialang.org/t/how-to-invoke-next-most-specific-method/7664 If you read the question, you'll see the |
I have come around to the view that
invoke
should be removed from the language. I wrote up this patch to explore what this change would look like in Base.invoke
can be seen as an abstraction violation: to use it, you need to know not just which function to call but what argument types it declares. Argument types are much more of an implementation detail, especially in julia. For example, one will often start withf(x::Any)
, then later realize this can be narrowed tof(x::Real)
. Doing this can breakinvoke
call sites, even if all call sites actually pass Reals.invoke
can be seen as an overly elaborate form of renaming (much like static overloading). You want to call a particular method, but to do it you have to write down its signature. Arguably, an isolated method that people want to call directly should just have its own name. For example, I often changed call sites as:write_each
is not a great name, but despite that the new code is way shorter and easier to write.In one case (BitArray
cat
) I was able to eliminateinvoke
just by adding another 1-line method, which I think is a better solution.In still other cases the "abstraction" of invoke didn't seem too necessary at all, for example
Here the idea of invoke was "acquire a lock, then do whatever else println was going to do". But it better not do anything except print all arguments followed by a newline! Ideally, yes you'd rather write a function call than duplicate this
for
loop, but on the other hand this clarifies why we're acquiring a lock (because there are multiple I/O operations inside).So I think we're better off without this. Obviously the code deletion and simplification of the language are also significant benefits.
invoke
is used in packages, but from what I can tell not an overwhelming number of times. Hopefully authors will weigh in.