How to implement custom mutating methods in Ruby?

Viewed 124

I am writing a gem where I would like to implement a conditional assignment in a C extension.

The idea is provide assignment for logical defined-or. This would only assign if the variable is nil. For example...

var = false
var //= 42   # var is false
var = nil
var //= 23   # var is 23

I found in the ruby/ruby repository the definition of abbreviated assignment:

There are also ||= and &&=. The former makes an assignment if
the value was nil or false while the latter makes an assignment 
if the value was not nil or false.

I did not manage to find a reference in the source code. (As referred in the comments below ||= is not an operator but just syntax sugar)

I tried to look on apidock.com the definition of some bang methods: e.g. Array#map!:

static VALUE
rb_ary_collect_bang(VALUE ary)
{
    long i;

    RETURN_ENUMERATOR(ary, 0, 0);
    rb_ary_modify(ary);
    for (i = 0; i < RARRAY_LEN(ary); i++) {
        rb_ary_store(ary, i, rb_yield(RARRAY_PTR(ary)[i]));
    }
    return ary;
}

I managed to follow the references to rb_ary_modify and rb_ary_store in array.c.

Would you know how to implement a mutating method such as:

require 'logic_or'

# Current behaviour
nil.lor('a') # => "a"
false.lor('a') # => false

a = nil # => nil
a.lor('a') # => "a"
a # => nil

# Expected behaviour
a = nil # => nil
a.lor!('a') # => "a"
a # => "a"

Update

As pointed out in comments, the previous title (How is Double pipe or equals (||=) implemented in ruby?) was not coherent with the content of the question

1 Answers

I don't think you can implement val //= 23, which Perl refers to as "dor" (defined-or). There's no underlying method call. Instead, val ||= 23 is compiled into...

if !val
  val = 23
end

val //= 23 would be...

if val.nil?
  val = 23
end

You can find the code for this in compile.c. You'd have to somehow hack the parser to add a new idDOROP opcode and change the grammar. It's not impossible, it's done in Perl all the time, but it's very messy.

I also don't think you can do val.dor(23). That would require assigning to self which isn't allowed.

class Object
  def dor(value)
    self = value if self.nil?
  end
end

Can't change the value of self
    self = value if self.nil?

Closest I can come up with is this.

class Object
  def dor(value)
    value if self.nil?
  end
end

obj = obj.dor(42)
puts obj

Which is a questionable improvement over...

obj = 42 if obj.nil?
puts obj

In general, //= is not necessary in Ruby. //= came about in Perl because Perl considers 0 and "" to be false. There's many occasions where those are valid values but would be obliterated by $var ||= $default. People got tired of writing $var = $default if !defined $var and //= was born.

In Ruby there are only two false values, nil and false. Everything else is true. var ||= default will do the right thing with 0 and "" so there's little need for //=.

If your design requires regularly distinguishing between nil and false often enough to write a new operator, change your design.

Related