When to use functions and when to define structs and use methods on them?

This is a code style question, there are many cases where you can use functions in place of a struct, for example

struct Printer {
  foo: String,
}
impl Printer {
  fn print() {
    println!("{}", self.foo);
  }
}

and

fn run(foo: String) {
  println!("{foo}");
}

are the same and the latter obviously makes more sense but where is the line in this kind of usage? Like in

struct Printer {
  foo: String,
  bar: String,
  baz: String,
}
impl Printer {
  fn print_all(self) {
    self.print();
    self.eprint();
  }
  fn print(&self) {
    println!("{},{},{}", self.foo, self.bar, self.baz);
  }
  fn eprint(&self) {
    eprintln!("{}", self.foo, self.bar, self.baz);
  }
}

and

fn print_all(foo: String, bar: String, baz: String) {
  print(foo, bar, baz);
  eprint(foo, bar, baz);
}
fn print(foo: String, bar: String, baz: String) {
  println!("{},{},{}", foo, bar, baz);
}
fn eprint(foo: String, bar: String, baz: String) {
  eprintln!("{}", foo, bar, baz);
}

the struct really reduces repetition in the code, and it also brings the print methods closer together

But there are cases that make structs less clean than functions too, especially with references, for example

struct Runner<'a, 'b> {
  ctx: &'a mut Context<'b>,
}
impl<'a, 'b> Runner<'a, 'b> {
  fn foo(&self)
}

and

fn foo(ctx: &mut Context)

So I'm asking how do you decide which pattern you use in your code? Thank you!

Use a type if you need to store and re-use state. For one-off computations that only depend on their arguments, use a free function.

Citation needed. Structs don't add any more complexity than strictly needed.

In my experience, this is not an issue, not sure where you are running into this, but if it's regular, then you are probably abusing the language.

1 Like

I edited the post, hopefully it makes it more clear

My answer is still the same. If you need to store and re-use state across calls, you probably need to make your own type to store that state in. If you don't, and you only want to perform a single piece of computation with an ad-hoc set of inputs, then use a free function.

What if the function is really long, to extract functions you also need to type out all the parameters again, is this a fair tradeoff?

I often use a type when I start to find the local state in a function is getting too complex. This leads to the principle: types are how you refactor state, just like functions are how you refactor instructions.

The guidance for using functions to refactor is pretty clear, if not all that helpful: whenever it helps! Unfortunately it's really difficult to give actually useful advice for this sort of thing, so like how any advice like "extract a function whenever it's longer than 30 lines" or whatever can only be a very general guideline that will torture your code into gibberish if you slavishly follow it, there's not really any hard rules for when to introduce a type.

Don't be afraid to introduce a type with all pub fields and keep the functions how they are, too! The only real concern there is it the type is public to the crate, and often then you can just mark it as #[non_exhaustive] so you can still add fields later.

It sucks that there's no solid answer to this ):

If there were good hard rules for programming, then we could just program them and be out of a job! :smile:

2 Likes

If I understand you correctly then if all your extracted little functions need all of the parameters of the original big function then the big function has not been split up in a sensible way.

If all those little functions need access to all the same data, or part of it, then it seems to me that it is better to put that data into a struct and attach those functions to it.

Of course if you need more than one copy of that data collection put it in a struct and make many instances of that struct.

It gets worse...

trait CanPrint {
    fn print(&self);
}

impl CanPrint for String {
    fn print(&self) {
        println!("{}", self);
    }
}

fn main() {
    "Hello, world!".to_string().print();
}

honestly traits aren't such a bad idea except for the limitations (namely async) and duplicate code

1 Like