Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Good practice to avoid mutating method parameters in Ruby?

Tags:

ruby

I understand that it's good practice to avoid mutating method parameters in Ruby (unless that's specifically the intention).

What is a simple coding rule of thumb (in terms of style, naming conventions, etc.) for how I should manage this?

For instance, is this good practice?

def array_of_three(a, b, c)
   d = a.dup
   e = b.dup
   f = c.dup
   # Do work on d, e and f
   return [d, e, f]
end

How should I name method parameters vs. the duplicates inside the function which can be mutated? (Assuming something like the above approach is "correct"!)

Thanks.

like image 511
Convincible Avatar asked Oct 19 '25 10:10

Convincible


1 Answers

Ruby's "good practices" generally boil down to not mutating data you don't own. The problem is that ownership can be somewhat hazy, so if in doubt you'll want to presume you don't own the data and make a copy if you need to mutate it.

The way bad behaviour manifests is an example like this:

a = [ 1, 2, 3 ]

do_stuff(a) # Works
do_stuff(a) # Doesn't do anything for some reason

a
# => [ ]    # Hey! Who did that?

Where let's say do_stuff was being a jerk and did this:

def do_stuff(a)
  while (v = a.pop)
    puts a
  end
end

That's damaging the arguments you're given, which is bad. Instead you should either pursue a non-destructive approach:

def do_stuff(a)
  a.each do |v|
    puts v
  end
end

Or instead make a copy if it's necessary to manipulate that structure, for example:

def do_stuff(a)
  a = a.uniq.sort
  while (v = a.pop)
    puts v
  end
end

A good way to test that your methods are behaving properly is to feed them frozen data in tests:

a = [ 1, 2, 3 ].freeze

do_stuff(a) # Fails trying to manipulate frozen object

I tend to write unit tests with aggressively frozen data (e.g. deep_freeze) to ensure that the arguments cannot be modified. This shakes out ownership problems really quickly.

If your function has a method signature like this:

def do_stuff(*a, **b)
  # ...
end

Then you will own both a (varargs) and b (kwargs) as they're broken out specifically for your method. In all other cases you need to be careful. This is where programming by contract is important as the caller of your method must know which arguments are handed over and which are not. The documentation must make this clear.

like image 104
tadman Avatar answered Oct 22 '25 02:10

tadman



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!