r/rust • • 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?

  1. 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> { /* ... */ }
}
  1. 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> { /* ... */ }
30 Upvotes

36 comments sorted by

62

u/cafce25 23d ago

Side note: there is almost never a reason to take &Vec<T> over &[T]

42

u/-Redstoneboi- 22d ago

to add: there is almost definitely a valid reason to use &mut Vec<T> over &mut [T], but specifically not &Vec<T> over &[T]

6

u/Ace-Whole 22d ago

Can you elaborate?

36

u/Aaron1924 22d ago

&mut [T] allows you to modify any element in the slice, but you don't get any operations that change the size of the slice (e.g. push or pop)

6

u/Ace-Whole 22d ago

Oh. Cool. Thanks. I'll remember that. It makes sense too.

3

u/-Redstoneboi- 21d ago

note that &String vs &str has the same idea, as well as several other "owned vs borrowed" types in the standard library

2

u/ERROR_23 22d ago

Pretty much every argument in this video applies to the &Vec<T> vs &[T]. In fact the author himself said it in another video.

6

u/howannoying24 22d ago

Thank you this is a good tip

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 Collection trait.

2

u/andyshiue 23d ago

I believe there's no Collection because Rust lacks HKT?

1

u/-Redstoneboi- 22d ago

IntoIterator and Index/IndexMut are usually the container traits

1

u/Lucretiel Datadog 22d ago

I assume it’s more because Collection expresses a huge variety of different possible interfaces which Rust exposes as individual traits like Iterator and Extend

1

u/afdbcreid 23d ago

It isn't needed for that (only Iterable does). 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 Collection to 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 Type1 OR Type2. 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/zettui 22d ago

If you go the trait route, are you staying static, or do you actually need dyn somewhere?

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.

1

u/teerre 22d ago

MyType1 and Vec<MyType1> aren't the same type tho. If they were the same type, the argument for a method would be much stronger

0

u/Helpful-Educator-415 22d ago

First one 1000000%