No method matching error when calling generic constructor with concrete implementer of abstract type

I am trying to implement several algorithms that share a branch-and-bound setup on top, but have different sub problems. To do this, I decided to create abstract types for the sub problems and make the branch-and-bound type generic, but I get an error that I just do not understand at all.

Here is a minimal reproduction of the problem:

struct Dataset
end
struct Duals
end

# === ABSTRACT FACTORY TYPE ===
abstract type SubProblemFactory end
abstract type SubProblem end

function make_sub_problem(factory::SubProblemFactory,
                          dataset::Dataset,
                          duals::Duals)::SubProblem
  error("make_sub_problem was not implemented")
end

# === CONCRETE IMPL ===
struct MySubProblemFactory <: SubProblemFactory
end

function make_sub_problem(factory::MySubProblemFactory,
                          dataset::Dataset,
                          duals::Duals)::MySubProblem
  MySubProblem(dataset, duals)
end

mutable struct MySubProblem <: SubProblem
  dataset :: Dataset
  duals :: Duals
end

# === HOLDS A SUB PROBLEM FACTORY ===
struct BranchAndBound{F <: SubProblemFactory}
  factory :: F
  dataset :: Dataset

  function BranchAndBound{F}(factory::F, dataset::Dataset) where F<:SubProblemFactory
    new(factory, dataset)
  end
end

function solve()
  dataset = Dataset()
  factory = MySubProblemFactory()
  bb = BranchAndBound(factory, dataset)
end

solve()
ERROR: LoadError: MethodError: no method matching BranchAndBound(::MySubProblemFactory, ::Dataset)
Stacktrace:
 [1] solve() at /tmp/test.jl:44
 [2] top-level scope at /tmp/test.jl:47
in expression starting at /tmp/test.jl:47

Where have I gone wrong here? The method clearly exists, as the constructor is defined for any subtype of SubProblemFactory, and MySubProblemFactory is indeed defined to be a subtype of that.

Not sure, but I think you need to specify the type parameter to BranchAndBound in solve()

Edit:

julia> methods(BranchAndBound)
# 0 methods for type constructor:

julia> methods(BranchAndBound{MySubProblemFactory})
# 1 method for type constructor:
[1] (::Type{BranchAndBound{F}})(factory::F, dataset::Dataset) where F<:SubProblemFactory in Main at REPL[9]:6
1 Like

This works:

julia> abstract type SubProblemFactory end

julia> struct MySubProblemFactory <: SubProblemFactory
       end

julia> struct BranchAndBound{F <: SubProblemFactory}
        BranchAndBound(f::F) where F<:SubProblemFactory = new{F}()
       end

julia> f = MySubProblemFactory()
MySubProblemFactory()

julia> BranchAndBound(f)
BranchAndBound{MySubProblemFactory}()

julia>

I think there is some problem with the way you defined the constructor. (the new{F}, the type parameter is missing there, as pointed above, I think that is the problem).

1 Like

Thanks. This worked:

function solve()
  dataset = Dataset()
  factory = MySubProblemFactory()
  bb = BranchAndBound{MySubProblemFactory}(factory, dataset)
end

The new{F} does not appear to be necessary.

1 Like

Yes, in one case you need to define the type parameter explicitly on construction, in the new{F} case, it is deduced from the type of the argument:

julia> struct A{T}
         i
       end

julia> A(1) # does not work
ERROR: MethodError: no method matching A(::Int64)
Stacktrace:
 [1] top-level scope at REPL[2]:1

julia> A{Int64}(1)  # explicitly defining the type parameter
A{Int64}(1)

julia> A(i::T) where T = A{T}(i) # deduces type from argument
A

julia> A(1) # now it works
A{Int64}(1)

julia>

It seems like this is a prime case for improving the error messages in Julia.

1 Like

Agreed.

I can’t help but notice that the code is very Java-esque.
In Julia, you normally can do without factories and just directly use a type constructor instead:

function make_sub_problem(sptype::Type{MySubProblem},
                          dataset::Dataset,
                          duals::Duals)
  MySubProblem(dataset, duals)
end

function make_sub_problem(sptype::Type{AnotherSubProblem},
                          dataset::Dataset,
                          duals::Duals)
  # implementation for AnotherSubProblem
end

Calls will look like

make_sub_problem(MySubProblem, dataset, duals)

The first argument is a DataType, not an instance of that type.

Ah, interesting. In the spirit of this question, it doesn’t seem like you even need to keep the sptype value around at all? Just make the struct be parameterized and don’t include any fields that mention the type.

As for Java, my most familiar language is Rust, and Rust even supports that pattern you mentioned, although you wouldn’t keep the type object around like I suggested. I just didn’t know enough Julia to know how to do that. In fact, you’ll find me on the Rust discourse, answering questions like you are doing here.

Hmm, maybe you do need the type object? This gives a weird error:

struct Dataset
end
struct Duals
end

# === ABSTRACT FACTORY TYPE ===
abstract type SubProblem end

function make_sub_problem{T}(dataset::Dataset, duals::Duals) where T <: SubProblem
  error("oops")
end
ERROR: LoadError: UndefVarError: make_sub_problem not defined
Stacktrace:
 [1] top-level scope at /tmp/test.jl:9
in expression starting at /tmp/test.jl:9

Like, yes, make_sub_problem is not yet defined? I’m in the progress of defining it?..

Right, functions cannot be explicitly parameterized in Julia, unlike C++ or Rust.

You can make a callable “factory” type of course. That will work just the same as a function but looks kind of foreign to Julia:

function make_sub_problem(T::Type{<:SubProblem}, dataset::Dataset, duals::Duals)
    T(dataset, duals)
end

subproblem = make_sub_problem(MySubProblem, dataset, duals)

# or
struct SubProblemFactory{T<:SubProblem} end

(::SubProblemFactory{T})(dataset::Dataset, duals::Duals) where {T}
    T(dataset, duals)
end

factory = SubProblemFactory{MySubProblem}()
subproblem = factory(dataset, duals)

Right, using a Type object seems a lot more natural than that. I’m still very confused about the error message though — why do I get an UndefVarError of all things?

Ah, right. Because the parser thinks you want to define an external type constructor make_sub_problem{T}(a, b) for a non-existing type. My previous example can actually be modified as follows:

struct SubProblemFactory{T<:SubProblem} end

function SubProblemFactory{T}(dataset::Dataset, duals::Duals) where {T}
    T(dataset, duals)
end

subproblem = SubProblemFactory{MySubProblem}(dataset, duals)

It’s weird / crazy that a constructor is allowed to return anything at all, not necessarily the type in its name, but it is how things are.

1 Like

So it thought I was trying to define a constructor… I’m not particularly impressed with these error messages.

Wait, doesn’t this mean I could still call the constructor of T without having a separate factory type?

Haha! It works.

struct Dataset
end
struct Duals
end

# === ABSTRACT FACTORY TYPE ===
abstract type SubProblem end

# === CONCRETE IMPL ===
mutable struct MySubProblem <: SubProblem
  dataset :: Dataset
  duals :: Duals

  function MySubProblem(dataset::Dataset, duals::Duals)
    println("success!")
    new(dataset, duals)
  end
end

# === HOLDS A SUB PROBLEM FACTORY ===
struct BranchAndBound{F <: SubProblem}
  dataset :: Dataset

  function BranchAndBound{F}(dataset::Dataset) where F<:SubProblem
    new(dataset)
  end
end

function foo_bb(bb::BranchAndBound{F}) where F<:SubProblem
  sub = F(bb.dataset, Duals())
end

function solve()
  dataset = Dataset()
  bb = BranchAndBound{MySubProblem}(dataset)
  foo_bb(bb)
end

solve()
success!