Ruby: How to Write a Bang Method, Like Map

Ruby: How to write a bang method, like map?

EDIT - Updated answer to reflect the changes to your question.

class Array
def stuff!
self[0] = "a"
end
end

foo = [1,2,3,4]

foo.stuff!

p foo #=> ['a',2,3,4]

Write two similar methods -one with bang, one without- while respecting DRY concept

You can also factor out the code for a single item, then be a little more verbose in the array methods. The left_outer_join_element() method here makes practical sense on its own and is reusable even for non-Array objects.

def left_outer_join(ary, &block)
self.map { |e| left_outer_join_element(e, ary, &block) }
end

def left_outer_join!(ary, &block)
self.map! { |e| left_outer_join_element(e, ary, &block) }
end

protected

def left_outer_join_element(element, ary, &block)
ary.each do |obj|
if yield element, obj
obj.keys.each do |key|
element[key] = obj[key]
end
break
end
end
element
end

How to create bang method that modifies the argument

You can modify the object passed as argument, but you have to use the appropriate methods to modify the receiver:

def preload! hash 
hash.replace(foo: 1, bar: 2)
end

h = {}
preload! h
h #=> {:foo=>1, :bar=>2}

Assigning a new hash to hash inside preload! just affects the hash variable inside the method, not the h variable outside:

def preload! hash 
hash = {foo: 1, bar: 2} # doesn't work as expected
end

Ruby: Is it true that #map generally doesn't make sense with bang methods?

Base object

Here's an array with strings as element :

words = ['hello', 'world']

New array

If you want a new array with modified strings, you can use map with gsub :

new_words = words.map{|word| word.gsub('o','#') }

p new_words
#=> ["hell#", "w#rld"]
p words
#=> ["hello", "world"]
p new_words == words
#=> false

The original strings and the original array aren't modified.

Strings modified in place

If you want to modify the strings in place, you can use :

words.each{|word| word.gsub!('o','#') }
p words
#=> ["hell#", "w#rld"]

map and gsub!

new_words = words.map{|word| word.gsub!('o','#') }
p words
#=> ["hell#", "w#rld"]
p new_words
#=> ["hell#", "w#rld"]
p words == new_words
#=> true
p new_words.object_id
#=> 12704900
p words.object_id
#=> 12704920

Here, a new array is created, but the elements are the exact same ones!

It doesn't bring anything more than the previous examples. It creates a new Array for nothing. It also might confuse people reading your code by sending opposite signals :

  • gsub! will indicate that you want to modifiy existing objects
  • map will indicate that you don't want to modify existing objects.

What does the map method do in Ruby?

The map method takes an enumerable object and a block, and runs the block for each element, outputting each returned value from the block (the original object is unchanged unless you use map!):

[1, 2, 3].map { |n| n * n } #=> [1, 4, 9]

Array and Range are enumerable types. map with a block returns an Array. map! mutates the original array.

Where is this helpful, and what is the difference between map! and each? Here is an example:

names = ['danil', 'edmund']

# here we map one array to another, convert each element by some rule
names.map! {|name| name.capitalize } # now names contains ['Danil', 'Edmund']

names.each { |name| puts name + ' is a programmer' } # here we just do something with each element

The output:

Danil is a programmer
Edmund is a programmer

Ruby: Reference materials to learn more about assigning values to self

First read the article in Wikipedia about self (even if it does not mention Ruby at all).

To make a long story short:

  • Ruby has borrowed a lot of concepts from other languages, and self comes from Smalltalk.
  • self is called in Smalltalk a pseudo-variable, which means it is variable, but it is set by the runtime environment, not by the program or programmer.
  • self references all the time the receiver of a message. super references the superclass of that message that is implemented by the method the reference super is in. (Glad that you did not ask for super).
  • self in Ruby (as in Smalltalk) references all the time the current object, and that may be an instance of a class or even a class itself. So if you define methods on the class-side (only callable on the class), even there self references the object, which is the class. So it is possible in Ruby to use only self, you never have to write down the name of the class to denote the receiver. That helps a little bit when refactoring.

If you have get all that, take a look at Metaprogramming Ruby which tells you some more tricks how to use self, classes, eigenclasses and some other interesting things.

Why are bang methods dangerous in Ruby?

There are two widespread meanings of "dangerous" in standard library and common gems:

  1. Method mutates the receiver, as opposed to returning a copy of the receiver. Example: Array#map!

  2. Method will raise an exception if its primary function can't be performed. Example: ActiveRecord::Base#save!, ActiveRecord::Base#create!. If, say, an object can't be saved (because it's not valid or whatever), save! will raise an error, while save will return false.

I usually add a third meaning to it in my code:


  1. Method will immediately persist data in the database, instead of just changing some attributes and hoping that later someone will save the object. Example: hypothetical Article#approve!

Ruby: Why does this way of using map throw an error?

&:foo may erroneously be seen as &: plus foo (terms like "pretzel colon" reinforce this mistaken view). But no method foo is being called here. &:foo is actually & plus :foo, the latter being a plain symbol.

When calling a method, &object (without :) invokes object.to_proc (which is supposed to return a Proc) and passes the returned proc as a block argument to the method.

object often happens to be a symbol and Symbol#to_proc's implementation would look somehow like this in Ruby: (it's actually written in C)

class Symbol
def to_proc
proc { |object, *args| object.public_send(self, *args) }
end
end

So this:

method(&:symbol)

effectively becomes this:

method { |object, *args| object.public_send(:symbol, *args) }

or, if method doesn't yield multiple values (like map), it's simply:

method { |object| object.public_send(:symbol) }

Obviously, you can't pass additional arguments via a symbol.

But ... object doesn't have to be a symbol. You could use another class with a custom to_proc implementation. Let's abuse Array for demonstration purposes:

class Array
def to_proc
method, *args = self
proc { |obj| obj.public_send(method, *args) }
end
end

This hack would allow you to write:

["foo\nbar", "baz\nqux"].map(&[:gsub, "\n", '-'])
#=> ["foo-bar", "baz-qux"]


Related Topics



Leave a reply



Submit