Ruby 无副作用的散列操作方法

Ruby method operating on hash without side effects

我想创建一个将新元素添加到散列的函数,如下所示:

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

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

add_new_value(numbers_hash)

我读到不变性很重要,有副作用的方法不是一个好主意。明明这个方法是在修改原来的input,我应该怎么处理?

我不一定同意你应该始终避免变异的论点。特别是在您的示例的上下文中,突变似乎是该方法存在的唯一目的。因此它不是 side-影响 IMO。

当一个方法在做一些不相关的事情时更改输入参数时,我会称之为不需要的副作用,并且方法名称也不会明显改变输入参数。

您可能更喜欢return一个新的散列并保持旧的散列不变:

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

def add_new_value(numbers)
  numbers.merge(four: "cuatro")
end

numbers_hash_2 = add_new_value(numbers_hash_1)
#=> {:one=>"uno", :two=>"dos", :three=>"tres", :four=>"cuatro"}

numbers_hash_1
#=> {:one=>"uno", :two=>"dos", :three=>"tres"}

引自Hash#merge的文档:

merge(*other_hashes)new_hash

Returns the new Hash formed by merging each of other_hashes into a copy of self.

Ruby 是一种具有某些功能模式的 OOP 语言

Ruby 是一种面向对象的语言。副作用在 OO 中很重要。当您在对象上调用方法并且该方法修改对象时,这是一个副作用,这很好:

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

Ruby 还允许函数式样式,其中数据转换时没有副作用。您可能已经看到或使用过 map-reduce 模式:

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

命令查询分离

可能让您感到困惑的是 Bertrand Meyers Command Query Separation Rule 发明的规则。这个规则说一个方法必须或者

  • 有副作用,但没有return值,或者
  • 没有副作用,但是return有点

但不是两者。请注意,虽然它被称为规则,但在 Ruby 中我会将其视为强有力的指导方针。有时违反此规则会产生更好的代码,但根据我的经验,大多数时候都可以遵守此规则。

我们必须澄清 Ruby 中“具有 return 值”的含义,因为每个 Ruby 方法都有一个 return 值——值它执行的最后一条语句(如果为空则为 nil)。我们的意思是该方法有一个 intentional return 值,它是该方法契约的一部分,调用者可以使用。

下面是一个具有副作用和 return 值的方法的示例,违反了此规则:

# 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

以及如何将其分为两种方法以遵守此规则:

attr_reader :valve_open

def open_valve
  @valve_open = true if @power_available
end

如果您选择遵守此规则,您可能会发现用动词短语命名副作用方法以及用名词短语命名 returning-something 方法很有用。这使得您从一开始就很清楚您正在处理哪种方法,并使命名方法更容易。

什么是副作用?

副作用是改变对象或外部实体(如文件)的状态。这种改变其对象状态的方法有一个副作用:

def register_error
  @error_count += 1
end

这种改变参数状态的方法有副作用:

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

这种写入文件的方法有一个副作用:

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