From time to time, when I create a code, people ask me: Is this working? , or they tell me: I didn’t know that. Being in those situations allows me to understand that something normal for me can be new for someone else. So, today I would like to share with you some Ruby tips and tricks. I hope you will like them.
1. dig
default value & nested hashes/arrays
By default, when you use the dig
method and the requested key is not in the hash, you will get nil
. But we can change this behavior and return whatever we want.
Default behavior:
hash = { foo: { bar: [:a, :b, :c] } }
hash.dig(:hello)
# => nil
Our new behavior:
hash = { foo: { bar: [:a, :b, :c] } }
hash.default_proc = -> (hash, _key) { hash }
hash.dig(:hello, :world)
# => {:foo=>{:bar=>[:a, :b, :c]}}
hash.dig(:hello, :world, :foo, :bar, 2)
# => :c
You can see one more interesting thing here. When you use the dig
method in the hash, it doesn’t matter if this is a simple hash only with keys and values like { foo: 1, bar: 2}
or a more complex one with nested hashes and arrays like in the example above, you still can get selected value.
hash = { foo: { bar: [:a, :b, :c] } }
hash.dig(:foo, :bar, 2)
# => :c
2. Quick debugging with tap
The most common usage of the tap
method for me is to use it as a quick debugger.
class Object
def debug
tap { |object| p object }
end
end
"foo".upcase.debug.reverse
# "FOO"
# => "OOF"
Thanks to this Object
class extension, we can see what an object looks like in the middle of the methods chain.
3. Difference between concat
and +=
for Array class
In most cases, we can use the concat
and the +=
methods interchangeably, but there is one important difference between them. Let’s check the example below.
The result we get from both methods is the same.
array = [1, 2, 3]
array.concat([4, 5, 6])
# => [1, 2, 3, 4, 5, 6]
array = [1, 2, 3]
array += [4, 5, 6]
# => [1, 2, 3, 4, 5, 6]
Now, let’s check what will happen when we use the tap
method with concat
and +=
.
array = [1, 2, 3]
array.tap { |a| a.concat([4, 5, 6]) }
# => [1, 2, 3, 4, 5, 6]
concat
behaves as we expected. We cannot say the same for +=
.
array = [1, 2, 3]
array.tap { |a| a += [4, 5, 6] }
# => [1, 2, 3]
What happened here? First, we need to understand the method tap
. Base on the definition from documentation we have:
tap
- Yieldsself
to theblock
, and then returnsself
. The primary purpose of this method is to “tap into” a method chain, in order to perform operations on intermediate results within the chain.
The main hint in this definition is and then returns self
. Let’s check the array
object.
array = []
array.object_id
# => 260
array += [4, 5, 6]
array.object_id
# => 280
In the case of +=
, we get a new object, but tap
is returning the object which was created at the beginning. Let’s compare that with the concat
method.
array = []
array.object_id
# => 300
array.concat([4, 5, 6])
array.object_id
# => 300
As you can see here, concat
returns the same object which was in the beginning. So, to summarize concat
append the new elements to the existing array, while +=
creates a new array with elements from both the original and additional array.
The last thing for the +=
method. To get the same result for +=
as we get for concat
, we can use the then
method instead of tap
. Definition for then
based on the documentation is:
then
- Yieldsself
to theblock
and returns the result of theblock
.
array = [1, 2, 3]
array.then { |a| a += [4, 5, 6] }
# => [1, 2, 3, 4, 5, 6]
4. split
with two arguments
We often use the split
method for strings like this:
"ruby:python:java".split(':')
# => ["ruby", "python", "java"]
but there is also a possibility to tell split
how many pieces you want to get:
"ruby:python:java".split(':', 1)
# => ["ruby:python:java"]
"ruby:python:java".split(':', 2)
# => ["ruby", "python:java"]
"ruby:python:java".split(':', 3)
# => ["ruby", "python", "java"]
5. String concatenation
There are a lot of possibilities to join strings. Some of them you can find below:
first_name = 'Agnieszka'
last_name = 'Małaszkiewicz'
name = first_name + ' ' + last_name
name
# => "Agnieszka Małaszkiewicz"
name = first_name << ' ' << last_name
name
# => "Agnieszka Małaszkiewicz"
name = "#{first_name} #{last_name}"
name
# => "Agnieszka Małaszkiewicz"
name = [first_name, last_name].join(' ')
name
# => "Agnieszka Małaszkiewicz"
We have one more way to join strings. We can use space between strings like this:
name = "Agnieszka" " " "Małaszkiewicz"
name
# => "Agnieszka Małaszkiewicz"
or simpler to see the pattern:
name = "Agnieszka " "Małaszkiewicz"
name
# => "Agnieszka Małaszkiewicz"
At first look, it can be strange that something like this can work in Ruby, but how many times did you split the line with a string? I use that for example in the test description. For example:
it 'calls DeliverCheckInInstructionsForProperty service for properties ' \
'with check-in instructions delivery enabled' do
# ...
end
Now the question: Why is it working? Based on what I found, it is related to Ruby source code in parse.y
. Based on the answer from Stack Overflow we have:
A Ruby string is either a
tCHAR
(e.g.?q
), astring1
(e.g."q"
,'q'
, or%q{q}
), or a recursive definition of the concatenation ofstring1
andstring
itself, which results in string expressions like"foo" "bar"
,'foo' "bar"
or?f "oo" 'bar'
being concatenated.
6. Create a hash with a default value
I write about this topic in Ruby Hash article in more detail, right now I want to mention two common uses of this feature.
First, you can declare the same default value for all keys in a hash. It’s pretty useful when we want to count something.
hash = Hash.new(0)
hash[:foo]
# => 0
Now we can count something by using +=
.
hash[:bar] += 1
hash[:bar]
# => 1
The second example is declaring a dynamic default value for each key in the hash. For example:
hash = Hash.new { |hash, i| i }
hash[1]
# => 1
hash[35]
# => 35
7. Use proc
in case
proc
is one of the classes in Ruby, which helps us with functional programming. One interesting usage of proc
, which is not often known, is using proc
in case
. We can create a proc
and put it directly to the when
condition of the case
. See the example below:
payload_1 = {
event_type: 'ConversationAdded',
body: 'Test'
}
payload_2 = {
event_type: 'MessageAdded',
body: 'Test'
}
payload_3 = {
body: 'Test'
}
def payload_object(payload)
message_type_event = proc { |event_type| event_type.include?('Message') }
case payload[:event_type]
when nil then 'Message'
when message_type_event then 'ConversationMessage'
else
'None'
end
end
payload_object(payload_1)
# => "None"
payload_object(payload_2)
# => "ConversationMessage"
payload_object(payload_3)
# => "Message"
8. Call method in different ways
Ruby is an awesome programming language. We can call a method in many different ways. If you are interested in how many ways we can find for calling method, I recommend you to read Gregory Witek’s article 12 ways to call a method in Ruby. I know two more ways to call a method, and I would like to share them with you. To be consistent with Gregory’s article, I will use the same example:
class User
def initialize(name)
@name = name
end
def hello
puts "Hello, #{@name}!"
end
def method_missing(_)
hello
end
end
The first way, I discover in Nick Schwaderer’s presentation from RailsConf 2022 Ruby Archaeology:
user = User.new('Agnieszka')
user::hello
# Hello, Agnieszka!
# => nil
The second way is related to functional programming. We can turn the method name into proc
and call on this proc
method ===
:
user = User.new('Agnieszka')
:hello.to_proc === user
# Hello, Agnieszka!
# => nil
If you want to know more about calling proc
by ===
, I recommend you to check my article about functional programming.
Need help?
If you're looking for a Ruby developer with over a decade of experience, don't hesitate to contact me.
I have experience in a variety of domains, with a focus on short user feedback loops and teamwork. I can help you build a great product.