Struct, tuple or plain numeric types for mathematical equations?

Warning: This is a slightly rambling, stream of consciousness on a certain aspect of library design in Rust. Any advice would be appreciated.

I’m writing a library that includes some implementations of mathematical equations that I’d like to be reusable for purposes other than my library’s direct goals.

To that end, I’d like them to be easy to adapt to whatever the user would like or whatever third party libraries they’re using. The simplest way would be to accept plain numeric types, like so:

fn do_equation1(x: i32, y: i32, z: i32)

So if a user is using another library that provides a Coord type, they can plug it in with do_equation(coord.x, coord.y, coord.z).

However as x, y, and z together form a type it’d make more sense to express that in the interface, perhaps using tuples like so:

fn do_equation2((x,y,z): (i32, i32, i32)) -> (i32, i32, i32)

Which, as shown above, also has the advantage of having symmetry with functions that return x, y and z. It can still be easily adapted to other types with an easy from or into implementation.

But if I’m going to use tuples then I might as well use a full struct, right? Except that feels more heavy weight even if it’s doing the same thing. A user that’s using another library’s Coord struct now has to juggle two different structs with the same layout and name but that are nonetheless different types.

So what do you think the best interface is in general? I’m minded to keep it simple. After all, the equations might turn out to useful for things that aren’t coordinates, even if I haven’t thought of them.

Are there any trade-offs I haven’t considered? Or am I way overthinking this?

For the input, you could use a variant of the “into trick”:

fn do_equation2(args: impl Into<(i32, i32, i32)>) {
    let (x, y, z) = args.into();

I’m not convinced that’s better than just having the caller call .into(), though.

I hadn’t considered that! But I’d agree that I’m unsure of the benefit.

Now that pattern-matching on arrays is stable, I’m not sure if I’d still use tuples for collections where all elements have the same type.

It’s been the historical practice, but ultimately array types scale better to many elements (O(1) characters vs O(size)), support the full slice interface, and have very nice convenience shortcuts like [0.; N]. With const generics (a first prototype of which recently landed in nightly), they are also likely to gain expressive power that won’t be available to tuples for a long while. And I think (but don’t quote me on this) that the compiler has a slightly easier time optimizing them too.

1 Like

You could do things like do_equation(coords: impl CoordLikeTrait) and implement the trait for tuples, and let others implement that trait for their types.

However, I would advise against doing it unless you really know it’s a problem you need to solve. It has a cost. Such layer of indirection makes documentation less clear. Working with abstract types is harder, as you need to declare everything about the type that you’ll ever need. All code touching it will become generic, and it might quickly get out of hand (e.g. CoordLike<AnyNumericType<NonNegative>>).

On balance I’m leaning toward using plain numbers. It looks a little bit more awkward in some circumstances but it’s simple for the user to create their own wrapper function if need be.

Documentation might be slightly clearer with tuples, especially if a function takes two sets of coordinates, but as this is essentially a “low level” interface to the underlying maths it does make sense to have as little magic as possible.

1 Like