Search code examples
modulejuliapolymorphismtypeerrorsubtyping

Julia - MethodError by using subtype of abstract type when definitions and use are in separate modules


Overview

A function in a separate module that annotates a parameter with an abstract type will throw an error when I pass a subtype.

Code below is summarized. Complete code is in following sections

# Module A 
abstract type State end
# Module B
using .A
struct transiting <: State
    from::Vector{<:Real}
    to::Vector{<:Real}
end
# Module C
using .A
function foo(op::State)::State
    return op
end
# Module Main
using .A, .B, .C

t = transiting([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])

foo(t) # error

The error received is MethodError: no method matching foo(::Main.Example.Subtype.transiting)

I was expecting to be able to pass any subtype through, as long as the struct declared what its super type was. This works as expected as long as the subtype and function that annotates the supertype (modules B and C) is placed in the same module, which doesn't make sense to me.

I have tried including all modules for both function declaration and subtype declaration and that doesn't work. I may just be unfamiliar with how Julia expects types to be organized, but I thought it was silly that it can't recognize that a subtype (explicitly a subtype) of a supertype isn't a subtype of the supertype.

Is this an actual problem or am I just expected to keep these definitions in the same module? The organization doesn't work well for what I am doing, and I'd prefer another solution.

Complete Code

# Module A
module Definition
export State

abstract type State end

end
# Module B
module Subtype
include("Definition.jl")
using .Definition

export transiting

struct transiting <: State
    from::Vector{<:Real}
    to::Vector{<:Real}
end

end
# Module C
module Func
include("Definition.jl")
using .Definition

export foo

function foo(op::State)::State
    return op
end

end
# Module Main
module Example

# include("Subtype.jl")
include("Func.jl")
include("Subtype.jl")
using .Func, .Subtype

t = transiting([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])

println(isa(t, State)) # false

foo(t) # error

println("Complete")

end

Solution

  • You're getting a MethodError, not a TypeError. Your code doesn't run; you've missed include("Definition.jl") and using .Definition in your Example.jl, so that State is defined.

    This behavior is expected. You should post (and look at) the full error:

    
        julia> false
        ERROR: MethodError: no method matching foo(::Main.Example.Subtype.transiting)
        
        Closest candidates are:
          foo(::Main.Example.Func.Definition.State)
           @ Main.Example ~/so/func.jl:7
    
    

    It tells you that the method definition is foo(::Main.Example.Func.Definition.State). If you do typeof(t) you'll get Main.Example.Subtype.transiting. If you now do supertype(typeof(t)) you'll get Main.Example.Subtype.Definition.State. So you have a method that expects a ::Main.Example.Func.Definition.State and you are passing a subtype of ::Main.Example.Subtype.Definition.State instead. Note that the crucial difference lies in Func.Definition.State versus Subtype.Definition.State.

    To avoid getting the namespaces mixed up, follow these two simple rules:

    1. Have the main module do all the using: using .SubModule1, using .SubModule2, etc.
    2. Inside a submodule, if you want to access something from a different submodule, grab it from the main module instead: using ..MainModule: TypeA, method_a

    Working code:

    example.jl:

    
        module Example
        
        include("definition.jl")
        using .Definition
        include("func.jl")
        using .Func
        include("subtype.jl")
        using .Subtype
        
        include("main.jl")
        
        export main
        
        end
    
    

    definition.jl:

    
        module Definition
        export State
        
        abstract type State end
        
        end
    
    

    func.jl:

    
        module Func
        
        using ..Example: State
        
        export foo
        
        function foo(op::State)::State
            return op
        end
        
        end
    
    

    subtype.jl:

    
        module Subtype
        
        using ..Example: State
        
        export transiting
        
        struct transiting <: State
            from::Vector{<:Real}
            to::Vector{<:Real}
        end
        
        end
    
    

    A few more things:

    • Do CamelCase for struct names: Transiting instead of transiting, so that everyone using the code will know that Transiting is a type instead of a function
    • Usually, source files containing module definitions have Capitalized names, while the others have lowercase names
    • You might find Reexport.jl to come in handy
    • You don't need to go through all the trouble of defining all these submodules. You can simply split your code in different files, which are all included in the main module definition, and you can manage what is in which namespace with using/import, again in the main module definition.
    • Don't do using X; prefer using X: XTypeA, XTypeB, xmethod_a, xmethod_b instead
    • You might like parametric types for your struct definition:
      
        struct Transiting{T<:Real} <: State
            from::Vector{T}
            to::Vector{T}
        end
      
      
    • JuliaFormatter.jl can help you adhere to a specific codestyle

    Simpler (working) example:

    Example.jl:

    
        module Example
        
        include("definition.jl")
        include("func.jl")
        include("subtype.jl")
        
        include("main.jl")
        
        export main
        
        end
    
    

    definition.jl:

    
        abstract type State end
    
    

    func.jl:

    
        function foo(op::State)::State
            return op
        end
    
    

    subtype.jl:

    
        struct Transiting{T<:Real} <: State
            from::Vector{T}
            to::Vector{T}
        end
    
    

    main.jl:

    
        function main()
            t = Transiting([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])
            println(isa(t, State))
            foo(t)
        end
    
    

    use in the REPL by executing:

    
        julia> using .Example
        
        julia> main()
        true
        Main.Example.Transiting{Float64}([0.0, 0.0, 0.0], [1.0, 0.0, 0.0])
    
    

    P.S. I'd suggest you edit the title of your question.