Skip to content

Add a non-throwing Strategies.parameter accessor, so consumers stop reinventing the NotImplemented catch #518

Description

@ocots

Summary

Strategies.parameter(T) throws NotImplemented by default, alongside id and metadata
same contract-enforcement pattern for all three
(src/Strategies/contract/abstract_strategy.jl:225-283). But parameter is the odd one out:
"this strategy has no parameter" is a legitimate, common answer, not an oversight. Every
non-parameterized strategy currently in the ecosystem already spells it out explicitly:

Strategies.parameter(::Type{<:CTSolvers.Integrators.SciML})    = nothing  # (non-parameterized branch)
Strategies.parameter(::Type{<:CTDirect.Collocation})           = nothing
Strategies.parameter(::Type{<:CTDirect.DirectShooting})        = nothing
Strategies.parameter(::Type{<:CTBase.Differentiation.AbstractADBackend}) = nothing

So nothing is already the fully-established, documented, non-ambiguous answer for "no
parameter" (parameter's own docstring gives parameter(Ipopt) → nothing as the example).
There is no second meaning nothing could be confused with here, so a new sentinel type would
not disambiguate anything real — it would just be a breaking rename of an already-working
convention, for every one of the 8 in-tree strategies above.

The actual gap

Any caller that wants to query parameter on a strategy it did not author — i.e. one that
might not have implemented the (optional) contract — has to catch NotImplemented itself.
OptimalControl does exactly this, in helpers/print.jl:330-343:

function _strategy_parameter(::Type{T}) where {T<:CTBase.Strategies.AbstractStrategy}
    return try
        CTBase.Strategies.parameter(T)
    catch e
        e isa CTBase.Exceptions.NotImplemented || rethrow()
        nothing
    end
end

This is not dead code to simplify away, and tightening the type bound does not remove the
need for it: parameter is optional-to-override on any AbstractStrategy subtype, including
ones not yet written, and Julia has no way to require an interface method at the abstract
type's definition site. A third-party discretizer/modeler/solver that forgets the override
throws regardless of how precisely the caller's own type is bounded.

Nor is there a cleaner alternative already available: hasmethod/applicable cannot
distinguish "concrete type overrode parameter" from "fell through to the AbstractStrategy
default", since the default fallback method always matches the signature. Method-identity
comparison against the fallback would work but is exactly the kind of fragile introspection
that already broke once this session (_strategy_base_name's UnionAll-depth assumption,
#516) — not a pattern worth encouraging a second time.

Requested

A non-throwing accessor living in Strategies itself, next to parameter, so this exact
try/catch is written once — in the package that owns the contract — instead of being
reinvented by every downstream consumer. Two reasonable shapes, either works:

Strategies.parameter(T, default)             # get(dict, key, default)-style 2-arg method
Strategies.parameter(T; strict::Bool=true)   # keyword opt-out of the throw

Either lets OptimalControl._strategy_parameter collapse to a one-line forward, and gives any
other package walking an AbstractStrategy registry the same safety without copying the
e isa NotImplemented || rethrow() idiom.

Not requested

A NoParameter sentinel type — see above, it would rename a working convention without
resolving any actual ambiguity.

References

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions