Caridorc
Caridorc

Reputation: 6661

Promising not to modify arguments in a Ruby function

Ruby allows you to state that a variable should not be modified by starting its name with an uppercase letter. Can you do this for formal function arguments too?

The interpreter should warn you if the value is instead modified.

As valid example:

def append(Constant xs, x)
     xs + x
end

An invalid example:

def append(Constant xs, x)
     xs += x
end

Another valid example:

def shift(Constant x)
     xs.first + xs.all_but_first
end

Another invalid example:

def append(Constant xs, x)
     xs.shift!
end

Upvotes: 0

Views: 595

Answers (2)

Jörg W Mittag
Jörg W Mittag

Reputation: 369556

Ruby allows you to state that a variable should not be modified by starting its name with an uppercase letter. Can you do this for formal function arguments too?

The interpreter should warn you if the value is instead modified.

I think you are confused. Ruby is pass-by-value, not pass-by-reference. You simply cannot modify the binding of method parameters in the caller's scope:

def foo(bar)
  bar = 'something else'
end

baz = "I won't change."

foo(baz)

baz
# => "I won't change."

To take the example added to the question:

# valid
def append(xs, x)
  xs + x
end

xs, x = 'Hello ', 'World'

append(xs, x)
# => 'Hello World'

xs
# => 'Hello '

# invalid
def append(xs, x)
  xs += x
end

xs, x = 'Hello ', 'World'

append(xs, x)
# => 'Hello World'

# as you can see, xs wasn't modified, since Ruby is pass-by-value:
xs
# => 'Hello '

What you can do, of course, is mutate the object:

def foo(bar)
  bar.replace('Hah! Of course you will!')
end

baz = "I won't change."

foo(baz)

baz
# => 'Hah! Of course you will!'

Note that this exactly equivalent to your constant analogy:

Baz = "I won't change"

Baz = 'something else'
# warning: already initialized constant Baz

but:

Baz = "I won't change"

Baz.replace('Hah! Of course you will!')
# no warning

Baz
# => 'Hah! Of course you will!'

As a caller, you can do one of two things:

  1. Copy the object you pass in, so that you keep an unmodified copy, in which case the method can do whatever it wants without harming you.

    foo(bar.clone)
    
  2. freeze the object you pass in, so that the method cannot modify it (but it might fail if it wants to).

    foo(bar.freeze)
    

As a method author, there is no way to communicate to either the caller or the language that you don't intend to mutate the arguments. Note, however, that mutating the arguments is considered extremely bad style, and in fact, I cannot name a single method either in the core lib, the stdlib, any code I have encountered, or any code I have written, even as a beginner.

So, in general you can assume that if the documentation doesn't mention it, then the method doesn't mutate the arguments, and conversely, that if the method did mutate the arguments, there would be a big fat warning to that effect in the documentation.

Note that having the language check that you don't modify an argument is simply impossible. You are just calling methods. How can the language know which methods modify the object and which don't? It would have to figure out whether a method can have any sort of externally observable side-effect on the internal state of the object or not. This, however, is equivalent to solving the Halting Problem and thus impossible.

Upvotes: 2

Wand Maker
Wand Maker

Reputation: 18762

You can freeze the object.

Prevents further modifications to obj. A RuntimeError will be raised if modification is attempted. There is no way to unfreeze a frozen object.

Example:

def meth a,b
    a,b = a.clone.freeze, b.clone.freeze
    a[:d] = 10  # Not allowed
end

meth({:a=> 10},"World")
#=> something.rb:3:in `meth': can't modify frozen Hash (RuntimeError)

As pointed in comments by @Jorg, to avoid the side effect, we may have to clone the parameter before freezing it.

Also note that you can assign new values to parameters a and b even if they are frozen, as assignment does not mutate the existing frozen objects - those assignments will not be visible outside the method.


Similarly, if you try to modify b:

b.replace("xyz")

will result in error:

something.rb:3: in `replace': can't modify frozen String (RuntimeError)

Upvotes: 4

Related Questions