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
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).
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>
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, 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.
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()