DEV Community

Cover image for Getting Cozy with Ruby
Ben Lovy
Ben Lovy

Posted on

Getting Cozy with Ruby

When I first approached Ruby, I basically looked at it like "dynamic C++", because that was the best analogue I had. Of course, that has required some tweaking. This post talks about some Ruby-specific idioms that aren't directly related to concepts I knew, and assumes you already use another class-based OOP language such as C++, Java, or Python.

Now that I've been using Ruby for about three days, I'm obviously a complete expert and to be implicitly trusted (heh). If you do see something wrong, please correct me!

Huge thanks to @dvik1950 on the Ruby exercism mentored track for many of these.

I know this splash image only mildly applies but it was too cool not to use. You can't change my mind.

Testing

This is a dynamic language. I am so very much a static kinda person, so unit testing is pretty much the first priority for not losing hair/sleep.

I found the easiest to start with is minitest:

$ gem install minitest
Enter fullscreen mode Exit fullscreen mode

Then, in my_math_test.rb:

require 'minitest/autorun'
require_relative 'my_math'

# MyMath sanity tests
class MyMathTest < Minitest::Test
  def test_times_two
    # skip
    assert_equal 8, MyMath.my_times_two(4)
  end
end
Enter fullscreen mode Exit fullscreen mode

Uncomment skip to skip the test, which avoids commenting/uncommenting the whole function. Also, the MyMathTest < Minitest::Test syntax is how you define a subclass, so MyMathTest inherits from Minitest::Test.

For a much more involved solution, there's rspec which provides a testing DSL instead of using Ruby functions. Here's what that test might look like:

describe MyMath do
   it "multiplies 4 by 2" do
     math = MyMath.new
     expect(math.my_times_two(4)).to eq(8)
  end
end
Enter fullscreen mode Exit fullscreen mode

This reminds me of using Mocha with Chai in JavaScript. There's an intro guide here.

%w

Don't do this:

JOES = ["average", "DiMaggio", "morning"]
Enter fullscreen mode Exit fullscreen mode

Do this:

JOES = %w[average DiMaggio morning]
Enter fullscreen mode Exit fullscreen mode

Whoa! Also, %i works for symbols:

SYMS = %i[one two three]
# [:one, :two, :three]
Enter fullscreen mode Exit fullscreen mode

Object.freeze

Not planning to ever change your JOES constant? Tell Ruby that you mean it and freeze 'em:

JOES = %w[average DiMaggio morning].freeze
Enter fullscreen mode Exit fullscreen mode

Now it's actually immutable! Read a lot more about Ruby constants here.

String interpolation

Basically, it does it. This works:

def say_name(name)
  output = "Hello, "
  output << name
  output << "!"
  puts output
end
Enter fullscreen mode Exit fullscreen mode

This is better:

def say_name(name)
  puts "Hello, " + name + "!"
end
Enter fullscreen mode Exit fullscreen mode

But you probably want this:

def say_name(name)
  puts "Hello, #{name}!"
end
Enter fullscreen mode Exit fullscreen mode

class << self

To redefine a method on self, so you can call MyClass.my_method, you can define it on self explicitly:

class MyClass
  def self.my_method(str)
    puts str
  end
end
Enter fullscreen mode Exit fullscreen mode

If you're doing this a bunch, you can open up the eigenclass, or singleton class, directly:

class MyClass
  class << self
    def my_method(str)
      puts str
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

I think that's a little less noisy, even at the cost of some extra lines and indentation. If you want to go super concise, you can just dot-operator your way all the way in:

class MyClass
end

def MyClass.my_method(str)
  puts str
end
Enter fullscreen mode Exit fullscreen mode

This class << self is also the best way to make a private method:

class MyClass
  class << self
    private

    def my_private_method(str)
      puts str
    end
  end
end
Enter fullscreen mode Exit fullscreen mode

Otherwise, you have to use private_class_method which I think looks gross:

class MyClass
  private_class_method def self.my_private_method(str)
    puts str
  end
end
Enter fullscreen mode Exit fullscreen mode

attr_accessor

You can publicly expose instance variables directly:

class MyClass
  def initialize
    @value = 0
  end
  def show_value
    puts @value
  end
end
Enter fullscreen mode Exit fullscreen mode

You define the constructor with initialize().

In Ruby, however, it's extremely easy to create getters and setters and usually preferable. You can manually do so:

class MyClass
  def initialize
    @value = 0
  end

  def value
    @value
  end
  def value=(new_value)
    @value = new_value
  end

  def show_value
    puts value # don't point to the `@value` var, send the `value` message
  end
end
Enter fullscreen mode Exit fullscreen mode

It's usually better to create both at once with attr_accessor:

class MyClass
  attr_accessor :value

  def initialize
    @value = 0
  end

  def show_value
    puts value
  end
end
Enter fullscreen mode Exit fullscreen mode

You can also use attr_reader or attr_writer for just the getter or just the setter, respectively.

The benefit is that now if this logic needs to change, all you need to do is define that method, and every call site automatically reflects the new logic.

Structs

You don't have to use nested arrays and whatnot for complex data just because we're using a dynamic language. Ruby provides a Struct class for structured data, which gives you these accessors methods automatically

This is a contrived example, but instead of this:

class MyRect
  attr_reader :rect_dims

  def initialize(arr)
    @rect_dims = arr
  end
  def show_size
    puts "Size: #{rect_dims[0]}x#{rect_dims[1]}"
  end
end
Enter fullscreen mode Exit fullscreen mode

Do this:

class MyRect
  attr_reader :rect_dims
  def initialize(arr)
    @rect_dims = make_dim_struct(arr)
  end
  def show_size
    puts "Size: #{rect_dims.width}x#{rect_dims.height}"
  end

  RectDims = Struct.new(:width, :height)
  def make_dim_struct(arr)
    RectDims.new(arr[0], arr[1])
  end
end
Enter fullscreen mode Exit fullscreen mode

As before, the whole point is to localize the definition of your data's structure to one single place, should it need to change again.

rubocop

Rubocop is a linter that adheres to the Ruby Style Guide. Do this:

$ gem install rubocop
Enter fullscreen mode Exit fullscreen mode

Then always do this:

$ rubocop myFile.rb
Enter fullscreen mode Exit fullscreen mode

Fix everything it says to fix, and if you don't understand what it's saying or why, look it up. Learning!

Here's a repl.it with the code from this post:

To be continued...

There's actually a bunch of cool stuff out there. Methods like gsub and inject stand out as some pleasant surprises from my first explorations. This comment from @ben has been spot on:

Ruby is a scripting language at its core. One command after another. Almost brutally simplistic.

I’d recommend opening up IRB and typing some commands. It’s actually pretty close to coding in the environment.

Ruby is sort of object oriented, sort of functional, and has a few ways to do the same thing most of the time. It’s a bit of a free-for-all.

There really is a bit of everything here. It's got a bit of pretty much every language I've previously used! Nailing down the clearest solution or most correctly applied pattern is a little tricker.

I looked at Ruby a long time ago, as one of my very first forays into programming. I liked how concise it was, and why's poignant guide is a really fun read - if you haven't read it, you should at least give it a try even if you're not necessarily targetting Ruby just for how unique it is.

However, I'm having a lot more fun with the language returning again as a more experienced coder. I think having the context I've built using a variety of different types of languages has helped me understand how to apply Ruby effectively. It was difficult for me to learn how to use Ruby at the same time as learning how to program in a general sense, despite how easy it is to get up and running.

I don't know that I'd personally recommend Ruby as a first programming language. There are so many paradigms available with so little friction. Do you agree or disagree?

Photo by Road Trip with Raj on Unsplash

Top comments (11)

Collapse
 
tadman profile image
Scott Tadman

That's a fun blitz tour of Ruby but you missed out on the big one, the "raison d'être" of Ruby, Enumerable.

One of the things that Ruby lends itself to quite naturally is transforming data from one form to another incrementally using little shifts, twists, and nudges. Like if you want to convert a string like "can_have_class" into "CanHaveClass", like the Rails String#camelcase method, you can do it with:

str.to_s.split(/_/).map do |s|
  s[0,1] = s[0,1].upcase
  s
end.join
Enter fullscreen mode Exit fullscreen mode

Where that splits the string apart at the underscores, converts each of those bits with map into a capitalized version of itself, then combines them back into a single string with join.

While this doesn't seem all that complex, it avoids a lot of the mess you get in other languages where you'd need to explicitly declare an array, use a for loop over the correct range, and remember to return the array when you're done.

If you compare Ruby to other languages superficially it has a lot in common, but Enumerable is a very interesting and unique feature. JavaScript has things like Lodash which try to emulate it, but only cover a fraction of the functions Ruby has and the syntax is way more clunky and awkward due to language limitations.

The only reason I mention this is because when approaching Ruby for the first time that's the philosophy you need to understand first, and everything else can flow from there.

Collapse
 
deciduously profile image
Ben Lovy • Edited

Very true, thanks so much for adding this! Blocks are indeed extremely natural and extremely common in all the code I read. It feels very much like using JavaScript or even a Lisp but wrapped up in a more class-based object-oriented structure. I think the strongest analogue I've used that's like that is ClojureScript - which is basically JS and lisp mashed together, so it stands to reason!

As side question, is this more or less what Scala feels like to use, just with types? Do some similar idioms arise via the functional<->OOP blending?

Collapse
 
tadman profile image
Scott Tadman • Edited

I haven't used Scala enough to comment on how it feels, but Ruby is able to straddle the intersection between procedural, object-oriented, and functional depending on how you use it.

This is increasingly true for a lot of languages, even Swift, Rust and Python, where functional approaches to problem solving have helped simplify things. Once you get lambdas it's inevitable that will happen. Maybe we'll even see more of that in C++.

Within Ruby there's a lot of push towards a more functional style as it's anticipated this will help resolve some of the major concurrency problems Ruby has. The Ruby core team seems to be opposed to threads as a general-purpose solution, instead steering towards approaches like Guilds, similar to how JavaScript has WebWorkers, but there's opportunities to parallelize things like map if you have clean, functional code to work with.

Here's hoping there's a concurrent version of things like map in future versions of Ruby!

Thread Thread
 
rhymes profile image
rhymes

This is increasingly true for a lot of languages, even Swift, Rust and Python, where functional approaches to problem solving have helped simplify things.

True that! My Python code is increasingly more functional and I think the natural namespace of file boundary in Python helps with that. Well, also having functions whereas Ruby has only methods. I don't use custom classes that much anymore in Python. Pass state between functions, inject dependencies, rinse, repeat.

It's also so much easier to test when you have side effects contained in a small amount of business logic.

Thread Thread
 
deciduously profile image
Ben Lovy

Rusts' Iterator trait definitely comes to mind, but I believe Ruby provides a greater set of methods, and I've definitely noticed a functional-forward trend in C++11 (and up).

I am not complaining :)

Thread Thread
 
tadman profile image
Scott Tadman

If someone ported Enumerable to Rust and/or JavaScript in all of its glory that would be amazing.

Collapse
 
ben profile image
Ben Halpern

A big love or hate with Ruby are the methods that objects have like...

array = []
array.empty? # true

num = 1
num.zero? # false
num = 0
num.zero? # true

I find it funny when a linter wants me to use .zero? as its so highly specific and odd to me. .positive? seems a bit more natural to me along the same lines.

Rails provides .first, .second, etc. up to .fifth for arrays and collections. In a trolling fashion, it also has .forty_two, which returns the forty-second element.

(1..200).to_a.forty_two # 42
Collapse
 
tadman profile image
Scott Tadman • Edited

There used to be more of these whereupon there was debate, and ultimately a cull. #forty_two is an artifact of that, if you can call it, process.

Collapse
 
deciduously profile image
Ben Lovy

Hah! That really does come off like a "screw you yes we did this".

I'm a huge fan of the wide variety of messages you can pass, but it does seem like it will take a while to learn everything that's available. They seem to lead to very readable code.

Collapse
 
andevr profile image
drew

Thanks for posting this. I recently started learning Ruby, been at it for about 3 weeks and loving it. I looked at the language before, kinda sorry I didn't try it sooner.

As a first language I'd probably recommend it because it just seems so easy. But then again, it might be better to try something harder like Java. That's what I wound up doing.

Collapse
 
tadman profile image
Scott Tadman

Don’t confuse “easy to learn” with “not hard to use”. If you’re challenging yourself with hard problems the language, so long as it’s suited to the task, is rarely the hard part.

I’m doing some heavy Ruby Async work now and it is far from easy. It’s just nice that Ruby is pleasant to work with vs. battling the C++ compiler and undefined behaviour instead.