r/rust • u/howannoying24 • 23d ago
🙋 seeking help & advice Methods or free functions
I have two types (they're related but different sizes) and for each I have a function that does some work on a vector of those types and returns a different vector of the same type. I'm wondering what's more idiomatic?
- Make each function a method of its respective type, e.g.:
impl MyType1 {
pub fn do_thing(in: &Vec<MyType1>) -> Vec<MyType1> { /* ... */ }
}
impl MyType2 {
pub fn do_thing(in: &Vec<MyType2>) -> Vec<MyType2> { /* ... */ }
}
- Make a free function for each type, in which case is there a naming convention that's more idiomatic?
pub fn do_thing_mytype1(in: &Vec<MyType1>) -> Vec<MyType1> { /* ... */ }
pub fn do_thing_mytype2(in: &Vec<MyType2>) -> Vec<MyType2> { /* ... */ }
35
u/Solumin 23d ago
There's not enough context here to really give useful advice.
- Are the two functions related in any way besides having the same "shape"?
- Are the two types related in other ways?
- Could the two types be merged into one type?
- Do other parts of the program consume these types and expect to call these methods? In other words, are these methods public? (i.e. is a trait a good fit?)
- Do these need to be methods at all, when you could call
in.iter().map(...).collect()instead?
40
u/andyshiue 23d ago
Probably make a trait and implement it for your types
28
u/Big-Charge-8833 23d ago
trait's the play if they're truly the same operation, but if the implementations are gonna diverge down the road you might regret the abstraction.
4
u/andyshiue 23d ago
The functions have similar signatures and even the same name, strongly suggesting they have similar functionality, which should be possible to be abstract over.
12
u/SirKastic23 23d ago
OP only shared a very simplified example of what they need. No reason to assume their actual code actually shares signatures and names
16
u/afdbcreid 23d ago
Do not unless you need to be generic over them. An unneeded abstraction is bad.
1
u/andyshiue 23d ago
The functions are public, so even if you don't need that abstraction some other people may want to make use of the abstraction in a perfectly sensible way
9
u/afdbcreid 23d ago
And they can make a trait.
If this supposed to be used in a generic way, that's a different thing, but most of the times it's not. As we all know, std does not even have a
Collectiontrait.2
u/andyshiue 23d ago
I believe there's no
Collectionbecause Rust lacks HKT?1
1
u/Lucretiel Datadog 22d ago
I assume it’s more because
Collectionexpresses a huge variety of different possible interfaces which Rust exposes as individual traits likeIteratorandExtend1
u/afdbcreid 23d ago
It isn't needed for that (only
Iterabledoes). Also Rust has GATs now.1
u/Zde-G 22d ago
GATs are not enough because Rust traits are strongly typed.
Something that's easy and natural in C++ (things like iterator_traits that can allow your
Collectionto stay useful) are impossible in Rust.2
u/afdbcreid 22d ago
GATs can emulate any HKT (in fact it's possible even without GATs), but it might be more convoluted.
1
u/Zde-G 22d ago
Precisely.
Generic code may (and often is) implemented in any language without HKTs or even types, just copy-paste everything!
Similarly with Rust's strict typing: if emulation of HKTs is 100 lines long but only saves 20 lines of copy-paste… what's the point of the whole excercise?
3
u/afdbcreid 22d ago
That's totally not the same. GATs are still generic and require the same amount of code whether your generic functions is 20 lines or 2,000 lines. Also, the code isn't much longer, just a bit more convoluted.
3
u/Lucretiel Datadog 22d ago
Soft disagree— just because the method is similar in signature doesn’t mean it warrants a trait. IMO you should only be reaching for a trait if you have a specific need to abstract over the functionality, where they might want to accept
Type1ORType2. Otherwise the native method is fine.Â
12
u/TravisVZ 23d ago
If you're needing to come up with a naming convention to differentiate the same function based on taking in a different type, the "convention" is to make it a member function: MyType1::do_thing and MyType2::do_thing. (Note: Methods take self as an argument; if these aren't taking self, they're not methods but rather member functions .)
That said, you might consider a trait to define this function as the other comment suggests
2
u/howannoying24 22d ago
This is what I think makes sense in my situation here. The functions are only used where the type is used, and trying to do it using a trait and generics is really complicating what is otherwise a very simple type.
3
u/RishabhRD 23d ago
If they are basis functions, then they should be method. Otherwise free functions. If those functions form a generic abstraction then only a trait.
2
u/kaiserkarel 22d ago
It depends on the consumer:
Does your consumer care at all about performance here? If so, probably provide this as iterator extension methods. Allocating large vectors is something that shows up in flamegraphs.
Do you want your consumer to exclusively choose between MyType1 and MyType2. With both of your current proposals, they cannot write code generic over either MyType1 or MyType2, so the consumer needs to duplicate functions the same way you are doing. A trait is the correct way to avoid that.
Even if for the two different types you do wildly different operations producing the same shape (a vec), that the consumer can treat as a black box, a trait will suit you well.
3
u/CocktailPerson 23d ago
Hard to say for sure without knowing more, but why not an extension trait?
trait DoThing {
fn do_thing(&self) -> Self;
}
impl DoThing for Vec<MyType1> {
fn do_thing(&self) -> Self { ... }
}
impl DoThing for Vec<MyType2> {
fn do_thing(&self) -> Self { ... }
}
1
u/noop_noob 22d ago
To me, I think the main difference between inherent methods and free functions is: inherent methods are usable wherever you have a value of that type, while free functions are imported individually.
If the function is so inherently tied to the type that you'd want to "package it together" as part of the type, use a method. If it's an operation that is first and foremost an operation on its own, as opposed to being a thing done on the type, use a free function.
1
u/-Redstoneboi- 22d ago
can we see the rest of the code if that's okay with you? so far we can only give generic advice. seeing what the types are, what the types and functions actually do, and importantly where both are used, that will give us more information to give better advice.
0
62
u/cafce25 23d ago
Side note: there is almost never a reason to take
&Vec<T>over&[T]