Search code examples
rubyhashimmutabilityside-effects

Ruby method operating on hash without side effects


I want to create a function that adds a new element to a hash as below:

numbers_hash = {"one": "uno", "two": "dos", "three": "tres", }

def add_new_value(numbers)
    numbers["four"] = "cuatro"  
end

add_new_value(numbers_hash)

I have read that immutability is important, and methods with side effects are not a good idea. Clearly this method is modifying the original input, how should I handle this?


Solution

  • Ruby is an OOP language with some functional patterns

    Ruby is an object oriented language. Side-effects are important in OO. When you call a method on an object and that method modifies the object, that's a side-effect, and that's fine:

    a = [1, 2, 3]
    a.delete_at(1)    # side effect in delete_at
    # a is now [1, 3]
    

    Ruby also allows a functional style, where data is transformed without side-effects. You've probably seen or used the map-reduce pattern:

    a = ["1", "2", "3"]
    a.map(&:to_i).reduce(&:+)    # => 6
    # a is unchanged
    

    Command Query Separation

    What may have confused you is a rule invented by Bertrand Meyers, the Command Query Separation Rule. This rule says that a method must either

    • Have a side effect, but no return value, or
    • Have no side effect, but return something

    But not both. Note that although it's called a rule, in Ruby I would treat it as a strong guideline. There are times when violating this rule makes for better code, but in my experience this rule can be adhered to most of the time.

    We have to clarify what we mean by "has a return value" in Ruby, since every Ruby method has a return value--the value of the last statement it executed (or nil if it was empty). What we mean is that the method has an intentional return value, one that is part of this method's contract and that the caller can be expected to use.

    Here's an example of a method that has a side-effect and a return value, violating this rule:

    # Open the valve if possible. Returns whether or not the valve is open.
    def open_valve
      @valve_open = true if @power_available
      @valve_open
    end
    

    and how you'd separate that into two methods to adhere to this rule:

    attr_reader :valve_open
    
    def open_valve
      @valve_open = true if @power_available
    end
    

    If you choose to adhere to this rule, you may find it useful to name side-effect methods with verb phrases, and returning-something methods with noun phrases. This makes it obvious from the start what kind of method you are dealing with, and makes naming methods easier.

    What is a side-effect?

    A side effect is something that changes the state of an object or or external entity like a file. This method that changes the state of its object has a side effect:

    def register_error
      @error_count += 1
    end
    

    This method that changes the state of its argument has a side effect:

    def delete_ones(ary)
      ary.delete(1)
    end
    

    This method that writes to a file has a side effect:

    def log(line)
      File.open(log_path, "a") { |f| f.puts(line) }
    end