Best practice for several alternative constructors

As a simplified example, suppose I have a type representing an axis-aligned ellipse defined by its major and minor axes:

struct Ellipse
    major::Float64
    minor::Float64
end

It makes sense to construct an ellipse from several different sets of parameters. My first thought was to implement something that allows the following usage:

Ellipse(major=2, minor=1)
Ellipse(semimajor=1, semiminor=0.5)
Ellipse(major=2, eccentricity=0.1)
Ellipse(major=2, focus=0.5)
...

However, this is impossible with separate methods: keywords do not participate in the dispatch. In principle it is possible to make one huge method with checks like

if kwargs[:major] != nothing && kwargs[:minor] != nothing
...

but it looks very error-prone: easy to miss a check or ignore a parameter by accident.

So my question is - what do you think is a reasonable interface in this case? How would you implement it?

You can take all kwargs in a single constructor, and check if they are just enough to determine the ellipse (and error for insufficient or overspecified information). See the implementation of range.

Thanks for the pointer, didn’t see this approach before:

range(start; length=nothing, stop=nothing, step=nothing) =  _range(start, step, stop, length)

_range(a::Real, ::Nothing, ::Nothing, len::Integer) = UnitRange{typeof(a)}(a, oftype(a, a+len-1))
# other well-formed implementations
# ...

_range(start,     step,      ::Nothing, ::Nothing) = # range(a, step=s)
    throw(ArgumentError("At least one of `length` or `stop` must be specified"))
# other errors
# ...

However, it looks really error-prone for more parameters: tracking the argument positions becomes more difficult, especially when you want to add or change one of the arguments.

You are right, but I would argue that having a lot parameters which are optional/required in various combinations is a code smell in any case.

Eg I don’t know if the Ellipse is just a MWE, but if it isn’t, arguably having both a major/minor and a semi-major/minor constructor may be overkill — after all, they are just a factor of 2 variation on the other one.

That said, you can do something like

Ellipse(; kwargs...) = _Ellipse((; kwargs...)) # make a NamedTuple

function _Ellipse(nt::Union{NamedTuple{(:major,:minor)},
                            NamedTuple{(:minor,:major)})
    Ellipse(nt.major, nt.minor)
end
# other methods like this
1 Like

Echoing @Tamas_Papp suggestion to not overkill this, especially if you are just trying to provide convenience to your users as you might end up doing the opposite.

That said here are some other options I can think of:

  1. Just define explicit methods for each valid combination, e.g. ellipse_major_minor(2,1), but I’m sure you have considered this already.
  2. Wrap the context of the numbers in a type OOP style, e.g. Ellipse(Major(2), Minor(1)), Ellipse(SemiMajor(1), SemiMinor(0.5)). If you don’t like the parantheses, make a macro which turns an expression like @Ellipse Major=2 Minor=1 into the above.

Both options suffer a bit from needing arguments in order (as well as overengineering) and you most likely need more than one method and/or some transform-one-type-to-another set of methods.

2 Likes

Thanks for all the suggestions! A nice one with NamedTuples, not sure if it’s “free” regarding performance?

I think I’ll go with separate methods indeed, and put them inside a module so that usage looks like:

Ellipse.from_major_minor(2, 1)
Ellipse.from_major_eccentricity(2, 0.1)
...
# the struct itself is Ellipse.Ellipse

Yes, it was just an MWE. The actual case involves of a more complicated object with several widely-used parametrizations.

Yes, it should be elided. I checked on 1.4 and 1.5.

There’s https://github.com/simonbyrne/KeywordDispatch.jl

3 Likes