RequiredInterfaces.jl icon indicating copy to clipboard operation
RequiredInterfaces.jl copied to clipboard

RequiredInterfaces bricks usage of Revise

Open briederer opened this issue 8 months ago • 2 comments

When using the Revise package in combination with the definition of an interface via RequiredInterfaces.@required changes in the source code immediately throws an error.

As an example I used a completely clean julia environment and added only the packages Revise and RequiredInterfaces, followed by loading Revise.

The test module is:

module TestModule

import RequiredInterfaces

abstract type AbstractDummyType end

RequiredInterfaces.@required AbstractDummyType begin
    foo(::AbstractDummyType)
    bar(::AbstractDummyType)
end

struct DummyType <: AbstractDummyType end

foo(::DummyType) = "foo"
bar(::DummyType) = "bar"

end

Calling Revise.includet("path/to/TestModule") throws the error

ERROR: LoadError: ArgumentError: `AbstractDummyType` is already registered as an interface.
Use the `begin` block version to specify multiple methods as part of the interface `AbstractDummyType`.
in expression starting at path/to/TestModule.jl:7
Stacktrace:
 [1] var"@required"(__source__::LineNumberNode, __module__::Module, T::Symbol, expr::Expr)
   @ RequiredInterfaces ...\.julia\packages\RequiredInterfaces\FQnDq\src\RequiredInterfaces.jl:47
 [2] eval
   @ .\boot.jl:430 [inlined]
 [3] process_source!(mod_exprs_sigs::OrderedCollections.OrderedDict{Module, OrderedCollections.OrderedDict{Revise.RelocatableExpr, Union{Nothing, Vector{Any}}}}, ex::Expr, filename::String, mod::Module; mode::Symbol)
   @ Revise ...\.julia\packages\Revise\mLfYT\src\parsing.jl:54
in expression starting at path/to/TestModule.jl:7

Unfortunately this makes development with Revise and RequiredInterfaces practically useless.

briederer avatar Apr 04 '25 08:04 briederer

This is halfway between intentional behavior and a limitation of julia/Revise of only being able to evaluate entire files at once (at least through includet). @required is intended to only be called once on a given abstract type, because interfaces are not generally extensible after their definition without breaking the API/users (types that implement the interface between such @required definitions have no way of knowing about the increase in API surface, and as such can't implement it properly). This is why multiple invocations are disallowed. Revise however doesn't know this/can only reevaluate entire files at once, so even if the interface declaration doesn't change, it expands the macro into the same module again, encounters the existing state within, and thus ends up with an error. To RequiredInterfaces.jl, these situations are (to my knowledge) indistinguishable, as Revise doesn't provide an interface to detect this kind of situation.

The workaround is to put the interface definitions into a separate file, so they only get reevaluated when the interfaces actually change, at which point you'll have to fix your implementors anyway.

Seelengrab avatar Apr 04 '25 09:04 Seelengrab

Thnks for this very clear explanation.

Totally makes sense to me. So probably a feasible workaround while developing the interface for the first time, would be to comment out the RequiredInterfaces part and once the interface is fully fixed it can be added.

And even if Revise would provide an interface (something like is_revised) this would probably require Revise to be included and thus be not a good ides for this lightweight package.

Anyways, would it be possible to add a part to the documentation that explains the Interplay with Revise and adds your suggested workaround? I guess that would be helpful for some others too.

briederer avatar Apr 04 '25 12:04 briederer