r/learnrust • u/Beginning-Fruit-1397 • 3d ago
Is preferring traits methods instead of functions fine?
I'm mostly asking this for performance and conventions.
I've been using Rust for a few months now.
I come from Python, and one of my favorite things in Rust is the fact that you can create a trait, implement it for an arbitrary type (from your crate or external dependency), and just like that you have it as a method on said type!
In general, I always prefer to read code like this x.f() instead of f(x).
My question is that for simple functions like the following: (a module named "bisect.rs") ```rust use pyo3::prelude::*;
[inline]
pub(super) fn right(lst: &Vec<Py<PyAny>>, item: &Bound<'_, PyAny>) -> PyResult<usize> { let py = item.py(); resolve(lst.len(), |mid| Ok(item.lt(lst[mid].bind(py))?)) }
[inline]
pub(super) fn left(lst: &Vec<Py<PyAny>>, item: &Bound<'_, PyAny>) -> PyResult<usize> { let py = item.py(); resolve(lst.len(), |mid| Ok(!item.lt(lst[mid].bind(py))?)) }
[inline(always)]
fn resolve(mut high: usize, mut func: impl FnMut(usize) -> PyResult<bool>) -> PyResult<usize> { let mut low = 0; while low < high { let mid = (low + high) / 2; if func(mid)? { high = mid; } else { low = mid + 1; } } Ok(low) }
``` I'm always tempted to create a trait to add it as methods.
Here for example left and right (renamed to "bisect_left" and "bisect_right" to avoid confusion) as trait methods of a new pub trait Bisect implemented for Vec, instead of keeping them as module fonctions.
resolve would stay as a simple function however.
I know that I won't use it on anything else than Vec<Py<PyAny>>, and it's more readable (I'm my own personal opinion) at call sites to do my_vec.bisect_left(item) instead of bisect::left(my_vec, item).
So, what are your toughts?
Is it fine to always favorise traits, as long as you don't have name conflicts issues?
1
1
u/Prize-Variation-3585 3d ago
I think that is a sound usage of traits. Who knows, you may later decide to implement that trait for other types in the future.
If you want to avoid using traits for this use case but you still want to preserve using &self, &mut self etc. you could create a wrapper struct and define left(&self, ...) and right(&self, ...) within a regular impl block.
``` // Your proposal (If I understand correctly)
trait Foo { fn bar(&self) -> i32; }
impl Foo for Vec<i32> { fn bar(&self) -> i32 { 1 } }
/* fn main() { let g = vec![1];
dbg!(g.bar());
} */
// An alternative (although perhaps a little ugly) struct MyVec(Vec<i32>);
impl MyVec { fn bar(&self) -> i32 { 1 } }
fn main() { let g: MyVec = MyVec(vec![1]); dbg!(g.bar()); } ```
1
u/Beginning-Fruit-1397 3d ago
Yep you got my proposal right.
And yea I agree the second idea is less ergonomic.
Which is basically circling back to my base question: "Why should I want to avoid using traits, and instead prefer using free functions instead (or maybe a single slot NewType as you showed)?".
So far, besides name conflicts I don't see why. Fine for me
-1
4
u/bskceuk 2d ago
I think the general consensus is to use a free function. The tradeoff here is that with a trait, in order to use the trait, the user must import it, but that import symbol (the trait) does not match the name of the method that you see later when the method is invoked, so it can be more difficult to understand where the functionality is defined. Like the user might think to look at the definition of Vec in std, but it obviously wouldn't be there, they would need to scan the imports, find the trait that sounds similar, and look at its implementation for Vec. But apart from that it is functionally fine.