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

Programmatically overloading methods #14910

Closed
omus opened this issue Feb 2, 2016 · 3 comments
Closed

Programmatically overloading methods #14910

omus opened this issue Feb 2, 2016 · 3 comments

Comments

@omus
Copy link
Member

omus commented Feb 2, 2016

I've written a package called Mocking.jl which allows developers to temporarily overwrite a method for testing purposes. Due to some changes in Julia 0.5 it appears that a method is not overwritten until the executing function is complete.

I've distilled Mocking.jl into a single method that should work in both 0.4 and 0.5:

function overwrite(body::Function, o::Function, n::Function, sig::Array{DataType}, restore::Bool=true)
    old_methods = collect(methods(o, Tuple{sig...}))
    new_methods = collect(methods(n, Tuple{sig...}))

    length(old_methods) != 1 && error("old function has too many methods")
    length(new_methods) != 1 && error("new function has too many methods")

    old_method = old_methods[1]
    new_method = new_methods[1]

    # Save the original implementation
    org_impl = old_method.func

    # Generate a method expression to overwrite the existing function

    if isa(old_method.func, Function)
        mod = old_method.func.code.module
        name = old_method.func.code.name
    else
        mod = old_method.func.module
        name = old_method.func.name
    end

    types = [:(::$t) for t in sig]
    expr = :($name($(types...)) = nothing)

    # Overwrite a method such that Julia uses updated implementation on future calls
    Core.eval(mod, expr)

    # Replace implementation
    old_method.func = new_method.func

    try
        return body()
    finally
        # Restore the overwritten function body with the original implementation
        if restore
            Core.eval(mod, expr)
            old_method.func = org_impl
        end
    end
end

rep(x::AbstractString) = x

Here is the expected behaviour as demonstrated in Julia 0.4.3:

julia> overwrite(open, rep, [AbstractString], true) do
          open("foo")
       end
WARNING: Method definition open(AbstractString) in module Main at none:1 overwritten in module Base at none:25.
"foo"

julia> open("foo")
ERROR: SystemError: opening file foo: No such file or directory
 in open at ./iostream.jl:90
 in open at iostream.jl:99

In Julia 0.5.0-dev+2444 the behaviour has changed:

julia> overwrite(open, rep, [AbstractString], true) do
          open("foo")
       end
WARNING: Method definition open(AbstractString) in module Base at iostream.jl:99 overwritten at none:25.
WARNING: Method definition open(AbstractString) in module Main at none:1 overwritten in module Base at none:25.
ERROR: SystemError: opening file foo: No such file or directory
 in systemerror(Base.#systemerror, ASCIIString, Bool) at /Users/omus/Development/Julia/latest/usr/lib/julia/sys.dylib:-1
 [inlined code] from ./c.jl:91
 in open(Base.#open, ASCIIString, Bool, Bool, Bool, Bool, Bool) at ./iostream.jl:90
 in overwrite(#overwrite, ##1#2, Any, Any, Array{DataType,1}, Bool) at ./none:34
 in eval(Core.#eval, Module, Any) at ./boot.jl:267

julia> open("foo")
ERROR: SystemError: opening file foo: No such file or directory
 in systemerror(Base.#systemerror, ASCIIString, Bool) at /Users/omus/Development/Julia/latest/usr/lib/julia/sys.dylib:-1
 [inlined code] from ./c.jl:91
 in open(Base.#open, ASCIIString, Bool, Bool, Bool, Bool, Bool) at ./iostream.jl:90
 in open(Base.#open, ASCIIString) at ./iostream.jl:99
 in eval(Core.#eval, Module, Any) at ./boot.jl:267

If we don't restore the old function you can see that the behaviour does change but only once we get back to the REPL (0.5.0-dev+2444):

julia> overwrite(open, rep, [AbstractString], false) do
           open("foo")
       end
WARNING: Method definition open(AbstractString) in module Base at iostream.jl:99 overwritten at none:25.
ERROR: SystemError: opening file foo: No such file or directory
 in systemerror(Base.#systemerror, ASCIIString, Bool) at /Users/omus/Development/Julia/latest/usr/lib/julia/sys.dylib:-1
 [inlined code] from ./c.jl:91
 in open(Base.#open, ASCIIString, Bool, Bool, Bool, Bool, Bool) at ./iostream.jl:90
 in overwrite(#overwrite, ##1#2, Any, Any, Array{DataType,1}, Bool) at ./none:34
 in eval(Core.#eval, Module, Any) at ./boot.jl:267

julia> open("foo")
"foo"

Is this new behaviour intentional? Should I be implementing this in a different way? Should method overriding be integrated into Base to ensure its continued availability?

@omus omus changed the title Programmatically Overloading Methods Programmatically overloading methods Feb 2, 2016
@JeffBezanson
Copy link
Member

It's an increasingly official rule that method changes aren't guaranteed to take effect until you return to the top level (see e.g. #4688). An alternate approach that might work is to use a macro that returns a :toplevel expression, such that the overwriting is done in one top level form and used in the next.

Should method overriding be integrated into Base disallowed entirely?

FTFY :) Perhaps!

@omus
Copy link
Member Author

omus commented Feb 2, 2016

Lol, thanks @JeffBezanson. I figured that integrating function overriding into Base wouldn't be popular.

@omus
Copy link
Member Author

omus commented Feb 12, 2016

I managed to solve this issue. The problem has to do with the call and not with the overriding.

@omus omus closed this as completed Feb 12, 2016
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants