API design help: potentially modifying argument



I would like to get comments on a certain point of API design for a package I am pondering over. Specifically, for an MWE, I have a generic function exposed by the API, the user defines custom types and how to “update” them with some change.

For some user-defined types, updating in place would make sense, while for others (fully immutable all the way down, eg containing StaticArrays) one would make a new one. I want to design an interface that accommodates both.

Option 1: single function interface

The interface exposed by the package could look like this

"Subtypes define [`updatestate!`](@ref)."
abstract type AbstractState end

    state′ = updatestate!(state::AbstractState, Δ)

Return `state` updated with `Δ`, possibly (but not necessarily) modifying the
first argument. It is up to the implementation whether `state′` and `state`
share structure. Functions in this package guarantee never to use assume that
`state` is unmodified after a call.
function updatestate! end

Then someone using the package defines a custom type, and implements the required interface:

# some implementation - defined by the user for a specific type

struct ConcreteState{T <: AbstractVector} <: AbstractState

# fallback, eg for StaticVector
updatestate!(state::ConcreteState, Δ) = ConcreteState(state.x .+ Δ)

# but Vector's we can modify
updatestate!(state::ConcreteState{<:Vector}, Δ) = (state.x .+= Δ; state)

Option 2: traits

Instead, user types could define a trait that describes whether they update in place (default: not). Depending on this, one would call updatestate or updatestate!.

Any other options? Comments? Examples I could look at?


I always advocate that f! can mean: f! has the “licence to modify” (and not an obligation).
That means that modifying functions should, when meaningful, return the modified argument. As example you can check the _chol! function underlying the Cholesky implementation which modifies matrices, but deals gracefully with StaticArrays and Number's so it can act recursively.