Why does Tuple not have AbstractVector semantics?

This came up along time ago, and I’m puzzling through it today:

We cannot use Tuples for DifferentialEquations.jl initial conditions, but everyone keeps asking for it because the syntax would be nice (and it would be fast). The issue is that tuples just don’t have enough algebra defined on them: a few overloads is all it needs. Is there a reason to not define these in Base?

6 Likes

I guess SVector is good enough if user want a Vector-like Tuple

  • No allocation if stable.
  • Similar constructor SVector(1, 2, 3) vs (1, 2, 3)

Yes, but is there a reason why Tuple couldn’t have some basic SVector semantics? I mean, I know I can tell people we don’t support tuple, but that just seems unnecessarily due to some random choice that I don’t know of a justification to.

3 Likes

I guess the justification is that a tuple semantically is not a vector and there is not reason why it should be. It’s simply a plain list.

2 Likes

Seems like it could grow beyond a “few” quickly. Once you decide that NTuple{N,Number} acts like a column vector, as opposed to just a generic container, it starts to be tempting to make more LinearAlgebra and mathematical functions work on it no? Where do you draw the line?

9 Likes

Sorry for the noob question: why isn’t it possible to accept tuples, but convert them immediately to arrays for further processing?

3 Likes

We can, we would need to also wrap all of their functions to do the automatic conversion as well though, since they would then write their differential equation to return a tuple, so we’d need to special case it for every equation type

1 Like

What’s the issue with having more linear algebra on it?

The same issue with extending DifferentialEquations to accept tuples … we have a vast number of functions for linear-algebra computations on vectors (not just ± and scaling). Do we add tuple methods for all of them?

No, that’s not the issue with DifferentialEquations.jl trying to use Tuples. The issue with DifferentialEquations.jl / all of SciML trying to make Tuples work with user inputs is that the core functionality is always higher order functions. Handling higher order functions has more to it because then the internal designs “escape” back out to the user much more easily. For example, I can silently change things to SVector, but then when the user’s ODE is ((λ,v),_,t) -> (v,-Vp(λ)), that would fail because they didn’t write a function that returned an SVector (the error would then be that +(::SVector,::Tuple) is not defined). So then I could do (u,p,t) -> SVector(userf(args)), but I’d only want to apply that if it’s a Tuple. But wait, what if the user defined their function as f(u::Tuple,p,t)? We can tell them that they over-typed their function, but if we actually want to make things “just work” what we should do is (u::SVector,p,t) -> SVector(userf(Tuple(u),p,t)). But wait, that would break codes that actually were SVector, so we’d actually need to keep a type parameter around OriginalWasTuple to know whether to dispatch into the tupling or not (or create a separate VecTuple type to keep it separate from SVector and dispatch on that). Do all of that and now one package supports tuples.

And yes, I will do this (and was in the middle of doing it which made me bring this up). But when you really write all of that out, doesn’t it feel like it’s just working around a language flaw that shouldn’t be there? I’d prefer to have tuples work in equation solver libraries by default as a nice simple syntax, but without agreement on adding some stuff to Base, it’s can’t happen without some pretty deep piracy (with some pretty major invalidations, so compile time issues). Thus the current state is that all solvers that want to support it have to build something to opt-in. So before I put all of the work in, I wanted to double check that everyone actually thought through the status quo and were okay with it. Sounds like people think this is okay, so I’ll just work around it.

This thread will at least be a good canonical source to when people will ask “why does DifferentialEquations.jl/NonlinearSolve.jl/… support tuples, but NLsolve.jl/QuadGK.jl/etc. don’t?”. It could be fixed, here’s the workaround, and it’s a choice to require the workaround. No matter the decision here, I hope that by making this question explicit, it makes this design choice much more clearly thought out as yes or no.

9 Likes

Is the fact that tuples allow heterogenous element types an issue? I see two possible issues, but I don’t know if they are a real problem. One would be that (1, 2.0) doesn’t promote while [1, 2.0] does which could be confusing for a newcomer. The second would be it seems things like heterogenously typed matmuls and dot products would be really slow or need to promote very eagerly which could be unexpected (or not).

5 Likes

I think of tuples as mostly structs with integer fields (with iteration as an oddity). If I add + and * and linear algebra to tuples, I’d add them to structs to match. It seems wrong to do that to structs, so I wouldn’t do it on tuples either.

4 Likes

that’s not a bad mental model at all, IIRC C# calls their named tuple as “anonymous type”, so Tuple can be understood as an anonymous type where values are accessed via offsets (index) rather than field names.

so yeah, my 2 cents is I don’t think Tuple should have semantics of Vector.

Also consider that when we return multiple variable from a function, we’re returning a Tuple, not a Vector (it’s not seen as a R^n member by the user probably)

3 Likes

Tuple will always be the weird one out, as long as they are covariant wrt. their type parameters, and they are heterogenous.

That is, what is the value of T in Tuple{String, UInt, Char} <: AbstractVector{T}? What is the element type?

We could perhaps have NTuples be AbstractVector, but then it would be confusing that tuples that happened to have the same element type would have completely different semantics than tuples with different element types.

I think the type hierarchy is simply too coarse a tool for this to work. AbstractVector promises too much, and no matter what we do, there will be some types that are ordered indexable containers but not AbstractVector. If we want some code to work with all these containers, either use duck typing or make a new trait.

Alternatively, just use StaticVector.

6 Likes

This! SVector is basically the tuple-as-vector structure, it exists already and is widely used. The suggestion in this thread is effectively to make ntuples behave as svectors, not sure why is that needed given SVector existence.

SVector enforces a single element type, which is not necessary for many operators like + and -. Also, Tuples are built into the language and have a privileged syntax so many users will automatically gravitate towards them. I linked one issue, but this is seen in the wild too:

So while it’s “obvious” to us that “oh yes, Tuples are not math objects, don’t do that here”, it’s clearly not obvious to general users who are picking up the language because they keep trying to do it.

Also, using StaticArrays is one of the single worst things you can do to startup times while Tuple is built in and instant. So SVector is a solution but I wouldn’t say it’s that effective at actually solving the problems for why this arises.

4 Likes

I am 100% in sync with @ChrisRackauckas here. I am in exactly the same situation with a package of mine: StaticArrays is the largest dependency latency-wise, but I use a small subset of its functionality (basically *, + and little else), and still I cannot support user inputs like e.g. x -> (x, cos(x)) where we would actually need x -> SA[x, cos(x)]. Having a select subset of linear algebra working on tuples would fix it for me, but that’s hardly a good reason to change something as fundamental as the very concept of a tuple (i.e. a heterogeneous, covariant collection of things). So I also agree with some of the opposed reasonings here. I don’t know what the solution is. I somehow feel that the right fix would be more introspection power into methods to be able to mutate the first input into the second without the huge hassle that Chris describes.

1 Like

@ChrisRackauckas, in your particular case, why wouldn’t it work to add a convert(SVector, userf(args)) before you pass the result of userf (provided it is a tuple) onto +? convert becomes a no-op if the result of userf(args) is already an SVector.

EDIT: I refer only to the first part of your problem, not the overspecialized f(::Tuple, ...) part

Then I would have to write a dispatch of every solver specifically for SVector, separate from just non-mutating out-of-place, since not every use of out-of-place is SVector. Doing it like this sounds like at least thousands of lines of code :sweat_smile:

1 Like

I sometimes use Tuples in place of vectors for quick stuff, and I do math like (3, 4.0) .- (1, 2.0). But I agree a tuple is a tuple, not a vector. What I would like to have is StaticArrays more accessible, or even better, have automatically optimized small arrays.