Hardscrabble 🍫

By Max Jacobson

See also: the archives and an RSS feed

why I think RuboCop is so cool, and how to contribute to it

August 9, 2015

RuboCop as compiler

Ruby is not a compiled language.

You can write code which has obvious flaws and Ruby will run it and then it will fail at runtime. For example:

puts "hello world"
puts hello world

That produces this output:

$ ruby lol.rb
hello world
/Users/max/Desktop/lol.rb:2:in `<main>': undefined local variable or method `world' for main:Object (NameError)

Some other languages wouldn’t even run that program. For example, the same code in Go:

package main

import "fmt"

func main() {
	fmt.Println("hello world")
	fmt.Println(hello_world)
}

Running that produces this output:

$ go run lol.go
# command-line-arguments
./lol.go:7: undefined: hello_world

But Ruby can’t run anything. Look at this bullshit:

puts "hello world"

def lol
  puts "lol world"

lol

Running that produces this output:

$ ruby lol.rb
/Users/max/Desktop/lol.rb:7: syntax error, unexpected end-of-input, expecting keyword_end

Notice: it doesn’t even output “hello world”; it just straight-up fails to run. You might say it doesn’t compile.

Ruby actually has a command line flag for checking the syntactic-correctness of a program:

$ ruby -c lol.rb
/Users/max/Desktop/lol.rb:7: syntax error, unexpected end-of-input, expecting keyword_end
$ ruby -c ok_program.rb
Syntax OK

This is useful, but only to a point. It means your programs will usually run, but you’ll have more errors at runtime than you would writing in a language like Go (for example).

I barely know Go, but in the small exposure I’ve had, I’ve really enjoyed how nit-picky the compiler is. For example, this program:

package main

import "fmt"

func main() {
	msg := "Hello world"
	other_msg := "lol world"
	fmt.Println(msg)
}

Running it produces this output:

# command-line-arguments
./lol.go:7: other_msg declared and not used

What! I can’t run my program because I declared a variable but then didn’t use it?? Who cares?? Go cares! And I kind of do too, now. Why have it if you don’t need it? Go kind of forces you to write really intention-revealing code and to clean up anything which might obscure your intentions. That’s great.

Ruby doesn’t care as much about that.

msg = 'hello world'
other_msg = 'lol world'
puts msg

That’s fine:

$ ruby -c lol.rb
Syntax OK
$ ruby lol.rb
hello world

So now your code has this random unused variable. It’s not really hurting anyone or anything. In theory it has a performance impact, as it’s allocating an object you don’t really need it to, and if this code is run a ton that could matter. But more importantly, I think, it’s just clutter. When people come across this code in the future they won’t know why it’s there, but they might assume it’s there for a reason, and they’ll mentally mark this area of the code base as kind of strange and unknowable.

RuboCop is a gem which can help bring Ruby closer toward Go levels of nit-pickiness, and I’m a huge fan of it. I recommend including it in your Ruby projects and running it alongside your tests to enforce adherence to its rules (“cops” in its parlance).

Running that same ruby program through RuboCop produces this output (note: you have to gem install rubocop first):

$ rubocop lol.rb
Inspecting 1 file
W

Offenses:

lol.rb:2:1: W: Useless assignment to variable - other.
other = 'lol world'
^^^^^

1 file inspected, 1 offense detected

Awesome! Now we know our code contains some offense and might be confusing our collaborators, and we know exactly where to make the change.


RuboCop as code style nit picker

RuboCop is also very opinionated about code style. For example, it will complain if you aren’t consistent about using single or double quotes, or if you aren’t consistent about using two spaces for indentation, or if you leave spaces at the end of your lines.

I recommend configuring it to your taste and to not feel guilty about disabling cops which you don’t find valuable. As a tool, it adheres to the “strong opinions, weakly held” mantra: it’s very easy to bribe this cop into changing its opinions by adding a simple yml file to the root of your project.

The Go compiler isn’t picky about things like code style, but Go ships with a secondary, optional tool called gofmt which is extraordinarily opinionated: it straight-up rewrites your code to follow Go style conventions.

Similarly, many RuboCop cops are auto-correctable. Consider this program a “before picture”:

class Dog

  def initialize(name:name)
    
    @name=name
    end
end

milo = Dog.new(name: "Milo")
p milo


Kind of ugly looking, but it’s syntactically valid:

$ ruby -v
ruby 2.0.0p451 (2014-02-24 revision 45167) [x86_64-darwin14.3.0]
$ ruby -c lol.rb
Syntax OK
$ ruby lol.rb
#<Dog:0x007fbe64207a40 @name="Milo">

So let’s try autocorrecting it:

$ rubocop lol.rb --auto-correct
Inspecting 1 file
W

Offenses:

lol.rb:1:1: C: Missing top-level class documentation comment.
class Dog
^^^^^
lol.rb:2:1: C: [Corrected] Extra empty line detected at class body beginning.
lol.rb:2:23: W: Circular argument reference - name.
  def initialize(name:name)
                      ^^^^
lol.rb:3:1: C: [Corrected] Extra empty line detected at method body beginning.
lol.rb:3:1: C: [Corrected] Trailing whitespace detected.
lol.rb:4:1: C: [Corrected] Trailing whitespace detected.
lol.rb:4:5: W: end at 4, 4 is not aligned with def at 2, 2.
    end
    ^^^
lol.rb:4:10: C: [Corrected] Surrounding space missing for operator =.
    @name=name
         ^
lol.rb:5:10: C: [Corrected] Surrounding space missing for operator =.
    @name=name
         ^
lol.rb:8:22: C: [Corrected] Prefer single-quoted strings when you don't need string interpolation or special symbols.
milo = Dog.new(name: "Milo")
                     ^^^^^^
lol.rb:9:22: C: [Corrected] Prefer single-quoted strings when you don't need string interpolation or special symbols.
milo = Dog.new(name: "Milo")
                     ^^^^^^
lol.rb:10:1: C: [Corrected] 2 trailing blank lines detected.
lol.rb:11:1: C: [Corrected] 2 trailing blank lines detected.

1 file inspected, 13 offenses detected, 10 offenses corrected

Afterwards, the program looks like:

class Dog
  def initialize(name:name)
    @name = name
    end
end

milo = Dog.new(name: 'Milo')
p milo

It’s not perfect. I’m surprised it didn’t autocorrect the indentation on the first end. But it’s neat that it did as much as it did.

EDIT: I opened an issue about this surprise and it turns out they don’t want to autocorrect that unless you really opt in.

In the past, I’ve written about how Ruby Keyword Arguments aren’t Obvious, and mentioned “circular argument references” as a mistake I had to learn not to make. In an attempt to give back to others like me, I submitted a pull request which adds a cop to RuboCop that checks for the presence of those circular argument references and warns you about them. This is particularly useful on Ruby 2.0.0 and Ruby 2.1, which don’t emit warnings about their presence as Ruby 2.2 does.


contributing to RuboCop

Contributing to RuboCop is somewhat intimidating because, as you might imagine, its code base has very high standards. It also seems like it ought to be very complicated, because the project needs to be able to statically analyze code in order to complain about it.

Fortunately, the maintainers are very clear and quick in their feedback, and happy to merge things which seem useful. And the code base has many examples of how to write a cop. A cop is a class which inherits from RuboCop::Cop::Cop.

require 'rubocop'
require 'active_support/all'
RuboCop::Cop::Cop.subclasses.count #=> 226
# (required active support for the subclasses method, which I love)

Once you subclass Cop, your class will be on duty and RuboCop will ask it if it’s offended by the code it’s analyzing. You just need to tell it which types of code you care about. RuboCop thinks of Ruby code as a tree of nested nodes, each having a type. For example, when you assign a local variable, RuboCop sees that as a node whose type is :lvasgn. Let’s say you wanted to write a cop where local variables aren’t allowed to be named “harold”. You would write that like this:

# encoding: utf-8

module RuboCop
  module Cop
    module Lint
      class Harold < Cop
        def on_lvasgn(node)
          local_variable_name, value = *node
          return unless local_variable_name == :harold
          add_offense(node, :expression, 'Do not name local variables harold')
        end
      end
    end
  end
end

The trickiest bit is learning how RuboCop “sees” code as nodes and learning what the different parts are called. That comes from looking through the existing examples and experimenting. It’s fun, I recommend it. I added one other cop on a bored evening, about discouraging option hashes.


RuboCop as teacher

This summer, I coordinated a group of 5 developer interns at work. I helped pick them out, so I knew they were all very bright but not super experienced with Ruby. We asked them to study a bit and follow the Rails Tutorial Book before starting to help get them on the same page, but we didn’t expect them to know Ruby conventions or best practices.

From the beginning, their project had RuboCop linting their code, with zero configuration. I warned them, “you’re going to hate this”. On their last week, I asked them if it was helpful, and they all said yes, but that it was often very annoying. Some of them liked it more than others. One even made a contribution to RuboCop fixing an issue in the cop I added. They produced a very interesting code base. Superficially, it’s immaculate. They picked up a few tricks from RuboCop suggestions. More than one cited guard clauses as a thing they wouldn’t know about otherwise.

The class length maximum (100 lines) and the method length maximum (10 lines) were both unhelpful. They’re meant to guide you toward following the single responsibility principle, but if it’s not something you’ve internalized, it just sort of makes you stuck and frustrated.

I came to think of RuboCop (and, later, coffeelint and scss-lint) as an automated layer of mentorship filtering out some potential questions that didn’t need to reach the human layer, so that layer could be reserved for more interesting problems and discussions.


RuboCop as totalitarian police state?

I do think of programming as a creative outlet, but don’t worry about linters inhibiting your creativity; think of it like a poetic form with strict rules, like the sonnet. There’s something very satisfying about consistency, right?

some helpful tmux aliases

August 1, 2015

EDIT October 25, 2023: I have an updated post that is a better read: My tmux aliases (2023 edition).

tmux is still an essential tool in my development workflow. Today I’m writing to share a few aliases/helper functions I’ve recently added to my dotfiles. Those change all the time so I’m hesitant to link to the files which the helper aliases and functions currently live in.

I added them in these commits, though: d06362c and c9d8695.

The implementation is mostly stolen from other people’s dotfiles and is gnarly to look at so I’ll just share how I use them:

EDIT February 8, 2016: I totally changed the implementation: 6d883df because I would occasionally have a bug with the old helper functions. More details in this issue

Here’s how to use these aliases:


When I’m not in a tmux session, and I want to see the list of tmux sessions, I used to run tmux ls. Now I run tl.


When I’m not in a tmux session and I’d like to start a new one, I used to run tmux new -s blog (where blog is the name of the new session). Now I run t.

It auto-chooses a session name based on the current directory’s name.

If there’s already a session with the name of the current directory, it cleverly attaches to that session instead of trying to start a new one with that name.


If I’m not in a tmux session and I’d like to attach to any existing tmux session, and I don’t particularly care which one because I’m planning to go into the session switcher anyway (C-b s), I used to run tmux a. Now I run ta.

stubbing constants in ruby tests

July 20, 2015

Let’s say you have some code that doesn’t have tests and you want to add tests. Because the code wasn’t written with tests in mind, it might be hard to write tests for it.

Last year, DHH wrote a blog post called Test-induced design damage which argued that code “written with tests in mind” (quoting myself from the previous paragraph, not his post) isn’t necessarily better than code written with other goals in mind, and can often be worse.

When I’ve attempted TDD, I’ve had times when I felt like it helped me write nice code and times where I think I went too far like if you’re rolling out some dough to make a pie crust but you’re watching TV and you end up spreading it out until it covers the whole counter.

So this code you want to test. What makes it hard to test? Let’s say it’s a Ruby class in a Rails app. In Rails apps, all the classes are available for all the other classes to reference and depend on. Maybe it looks like this:

class PieCrust
  def initialize(pounds_of_dough)
    @lbs = pounds_of_dough
    @ready = false
  end

  def prep
    RollingPin.new.roll_out(self)
    Oven.new.preheat
    Instagram.upload(self)
    @ready = true
  end

  def ready?
    @ready
  end
end

(This example is revealing more about my state of mind right now than anything)

But like, look at this thing. How do we write a test that covers all that? And what if we want “fast tests”?

(Note: a lot of people really want their tests to run fast. For TDD enthusiasts, this allows a tight feedback loop between when you write the failing test to when you write the code which makes the test pass. They kind of expect to do that over and over and over and don’t want to wait for more than an instant. I don’t think they’re wrong to want that although I am personally often OK with waiting for longer than an instant.)

(Other people want their tests to run fast as a general principle, like they want their cars to go fast or their legs to.)

Let’s say there are a couple hundred classes in your app and a bunch of initializers which run whenever your Rails application is loaded and none of them are strictly necessary for you to feel confident that your PieCrust class is behaving properly. All you want to know is that calling the prep method rolls out the crust, preheats the oven, and uploads the pie crust to instagram.

You already know that all those things work as long as you call them properly because you have unit tests for those classes demonstrating how to call them properly. You can see that here they’re being called properly, so you don’t feel the need to actually load the rolling pin code, or the oven code, or the instagram code. And you really don’t want to upload something to instagram every time you run your tests.

What do you do?

There’s the dependency injection approach, where you might refactor the earlier code to look like:

class PieCrust
  def initialize(pounds_of_dough, roller: RollingPin.new, heater: Oven.new, photo_sharing_service: Instagram)
    @lbs = pounds_of_dough
    @roller = roller
    @heater = heater
    @photo_sharing_service = photo_sharing_service
    @ready = false
  end

  def prep
    roller.roll_out(self)
    heater.preheat
    photo_sharing_service.upload(self)
    @ready = true
  end

  def ready?
    @ready
  end

  private

  attr_reader :roller, :heater, :photo_sharing_service
end

Which lets you leave your other application code the same – it can interact with PieCrust the same as it did before, as the default values are totally sensible there. But you can now write a test like this:

RSpec.describe PieCrust do
  describe '#prep, #ready' do
    it 'rolls the crust, preheats the oven, and uploads the photo' do
      roller = double
      heater = double
      photo_sharing_service = double

      pie_crust = PieCrust.new(10, roller: roller, heater: heater, photo_sharing_service: photo_sharing_service)
      expect(pie_crust).to_not be_ready

      expect(roller).to receive(:roll_out).with(pie_crust)
      expect(heater).to receive(:preheat).with_no_arguments
      expect(photo_sharing_service).to receive(:upload).with(pie_crust)

      pie_crust.prep

      expect(pie_crust).to be_ready
    end
  end
end

I feel like this is OK but it feels like it prescribes and duplicates a lot of the stuff that’s going on in the application code, which doesn’t feel ideal to me but also feels kind of fine.

Is there any other way? There is. I learned this one from my brilliant coworker Máximo Mussini. While looking through a gem he made (Journeyman, a lightweight replacement to FactoryGirl), I discovered some super interesting code, and without his blessing I extracted it out into a gem which I may use to help me write tests in the future. That gem is called stub_constant, and using it, I would revert that code to the first version, avoiding the arguably awkward dependency injection.

You might be assuming: OK, so if you don’t inject the constants, you must load the entire application environment, because you’re going to be depending on those dependencies. Or like, maybe you don’t load the entire application environment, but you must at least load the code which defines those 3 constants, right?

Nope! Doing that is usually really difficult, because once you load the files which define those constants, those files are probably referencing other constants, so you need to load the files which define those constants and now you might as well just load the whole thing…

So… “Whaaaat?” You might say.

Here’s what the constant-referencing isolation tests would look like:

require "stub_constant"
StubConstant.klass(:Oven)
StubConstant.klass(:RollingPin)
StubConstant.module(:Instagram)

RSpec.describe PieCrust do
  describe '#prep, #ready?' do
    it 'rolls the crust, preheats the oven, and uploads the photo' do
      roller = double
      expect(RollingPin).to receive(:new).with_no_arguments.and_return(roller)

      heater = double
      expect(Oven).to receive(:new).with_no_arguments.and_return(heater)

      pie_crust = PieCrust.new
      expect(pie_crust).to_not be_ready

      expect(roller).to receive(:roll_out).with(pie_crust)
      expect(heater).to receive(:preheat).with_no_arguments
      expect(Instagram).to receive(:upload).with(pie_crust)

      pie_crust.prep

      expect(pie_crust).to be_ready
    end
  end
end

Sooo it got even more prescriptive. But it’s a pretty neat way to do a purely isolated test without needing to rewrite your code.

How would you test this? I want to know.

making a pull request to rails

July 14, 2015

Today I impulsively made a pull request to Rails, which feels kind of like a milestone for me. It’s about two years since I started using Rails at the Flatiron School. It’s also been about two years since the method I edited was last edited. I feel like there may be a reason and it won’t get merged, but who knows? I feel sort of exposed.

This post was helpful for me: Eileen Codes | Getting Your Local Environment Setup to Contribute to Rails.

In order to make a proper contribution, I needed to know that my change didn’t break the existing tests, and so I needed to be able to run the tests.

I also wanted to be able to add tests and confirm that they pass. So I really needed to be able to run the tests.

I had some trouble configuring my local environment, despite the post explaining it well (…databases…), BUT the post mentions rails/rails-dev-box which lets you skip a lot of the environment configuration by using a preconfigured virtual machine, and that turned out to be a god send of a casual aside for me and I’m writing this post largely to promote the existence of that project because it’s awesome.

It uses vagrant which is kind of magical… I had never used it before and it totally blew my mind. It allowed me to have a tmux session with windows like I’m used to, with the code open in vim in one tab using all the existing configuration from my local Mac machine, and then another tab where my code changes were immediately available for running the tests against in the Linux virtual machine. It was super seamless and sweet.

Here’s a four minute long gif of what it looks like – I’m refraining from embedding it so you don’t need to download a 4mb image if you don’t want to, and so you can open it in a new tab to start at the beginning easily if you want to.

I don’t really love my solution and I should probably consider it further, but I know the tests are passing, including the new one I added. I think being a little sleep deprived lowered my inhibition tonight.

Quick tip: track command history in zsh

July 11, 2015

I switched from bash to zsh a few months ago and it’s been mostly sweet. I noticed that it wasn’t tracking my command history, so I did a little googling and got it working by adding these commands to my ~/.zshrc:

export HISTFILE=~/.zsh_history
export SAVEHIST=1000

I picked 1000 kind of randomly; a bigger number would probably be fine or nice.

using method_missing with class methods in Ruby

July 8, 2015

Ruby’s method_missing let’s you write some weird code:

class Poet
  def initialize
    @words = []
  end

  def method_missing(message, *args, &block)
    @words << message
    message.to_s.end_with?("?") ? sentence : self
  end

  private

  def sentence
    @words.join(" ")
  end
end

puts Poet.new.why.not.go.for.a.walk?

I’ve seen method_missing used to handle unexpected message on instances of a class before, but never for class methods. But, like, why not?

class LoudSpeaker
  def self.method_missing(message, *args, &block)
    if [:exclaim, :yodel, :howl, :sob, :beg].include?(message)
      puts args.first
    else
      # we don't want to handle this missing method, we want Ruby to raise the
      # NoMethodError it ought to
      super
    end
  end
end

LoudSpeaker.exclaim "helloooo!"

It’s just methods, so go for it.

todo_lint: a new ruby gem to help prevent comments from stagnating

July 8, 2015

I made another gem! Here it is: https://github.com/maxjacobson/todo_lint

Last week, I wrote about required_arg, my new gem for adding required keyword arguments to Ruby 2.0.0. I actually made that, quickly, while making this one, which took several hours longer.

What does it do?

It analyzes your code and warns you about TODO comments. You can run the script automatically as part of your continuous integration (CI) build process, and it will fail your build if your TODOs are troublesome. It suggests a workflow whereby your TODO comments must be annotated with a due date, and that due date must be in the future. So you can write little notes to self in your code, but you cannot forget them, because one day your build will start failing. At that point, you can do what you’ll do; maybe you’ll snooze the todo due date into the future; maybe you’ll pull the idea out into your feature/ bug tracker; maybe you’ll just delete it in acknowledgment that you’re never gonna do it. Up to you.

It’s going to be very annoying, and hopefully that will be a good thing.

Adding it to a Ruby project is pretty simple. Here’s what it looks like to add it to a gem: https://github.com/maxjacobson/film_snob/pull/85

At work, I’m coordinating a gaggle of interns working on a new project, and I asked them to be guinea pigs for me and include this gem in their project and CI workflow. They obliged, but unfortunately it immediately didn’t work at all. I didn’t realize it, but our CI service was bundling all dependencies into the same folder as the app, and then todo_lint was checking every file from every Ruby dependency for todos. We don’t want to check all those files, because they’re out of our control. I realized we would need some kind of configuration to allow excluding by folder or maybe even pattern, so I jotted it down in a GitHub issue and told the intern to forget about it; the guinea pig was dead!

Or … was it? Awesomely, she took the time to make a pull request to todo_lint, making it user-configurable; specifically, users can now exclude file patterns like vendor/**. Excellent! The guinea pig lives.

If you’d like to help improve the project, take a look at the remaining open issues and feel free to claim one or add your own. I want my projects to be friendly toward contributors. Awesomely, bundler now suggests including a code of conduct when you create a skeleton gem with, eg, bundle gem todo_lint, and I’ll do my best to uphold that.

Join me in being a scold. It’s fun.

required keyword arguments in Ruby 2.0.0

June 29, 2015

TLDR: I made a gem, required_arg which offers a workflow for requiring keyword arguments in Ruby 2.0.0, which doesn’t support them on the language level.

In March, we looked at Ruby keyword arguments, and noted a curious tension.

Sometimes you want to use keyword arguments, and you don’t want to come up with a default value for that keyword. You kind of want to require the keyword. You can in Ruby 2.1+, but Ruby 2.0.0 is in the awkward position of having keyword arguments but not being able to require them.

Here’s what you can do in Ruby 2.1:

class Dog
  def initialize(name:)
    @name = name
  end
end

Dog.new(name: "Milo") #=> #<Dog:0x007fc404df9f10 @name="Milo">
Dog.new #=> an exception is raised: ArgumentError: missing keyword: name

That’s great! You don’t need to write any additional code, and Ruby will enforce that your method is called with the keyword arguments you require. This gives you flexibility to design interfaces which take advantage of the flexibility and clarity of keyword arguments, but still establish some expectations for how the method will be called.

Here’s what happens when you do the same in Ruby 2.0.0:

class Dog
  def initialize(name:)
    @name = name
  end
end
# dog.rb:2: syntax error, unexpected ')'
# dog.rb:5: syntax error, unexpected keyword_end, expecting end-of-input

Syntax error!

Here’s what I suggest doing now:

  1. Upgrade to a newer version of Ruby
  2. If you can’t, try this:
# gem install required_arg
require "required_arg"

class Dog
  def initialize(name: RequiredArg.new(:name))
    @name = name
  end
end
Dog.new(name: "Milo") #=> #<Dog:0x007fc404df9f10 @name="Milo">
Dog.new #=> an exception is raised: missing keyword: name (ArgumentError)

Close enough!

If your app is stuck on Ruby 2.0.0 or you’re making a library which supports Ruby 2.0.0, maybe you’ll find this useful. Let me know if you do.

Here’s the entire source for the gem:

class RequiredArg
  def initialize(name = nil)
    msg = name.nil? ? "missing keyword" : "missing keyword: #{name}"
    raise ArgumentError, msg
  end
end

Pretty simple, and kind of fun. It’s just a little cherry bomb class. The moment you instantiate it, it blows up. Make it the default value for a keyword argument, and the moment you forget a keyword argument, the default will be used and the expression will be evaluated for the first time. It’s cool that the default values are lazily evaluated because it allows for things like this.

Check out the gem: https://github.com/maxjacobson/required_arg

assigning values to multiple variables in one line

June 27, 2015

Why would you write this:

a = 1
b = 1

When you could write:

a = b = 1

A few reasons:

  1. Maybe you don’t know about this syntax
  2. Maybe you don’t mind writing out two lines
  3. Maybe you’re concerned about having two references to the same data, as explained in this StackOverflow post

I recently saw code that looked like this, which was disabling some loggers:

Something.logger = OtherThing.logger = nil

And I was kind of confused and amazed. I know about this multiple assigning syntax, but this looked kind of different. In the earlier example, we were assigning a value to a simple local variable, but in this case we were calling a setter method instead.

Something like:

class Dog
  attr_reader :name, :family

  def initialize(name)
    @name = name
  end

  def family=(family_name)
    @family = family_name
  end
end

milo = Dog.new("Milo")
lola = Dog.new("Lola")

milo.family = lola.family = "The Jacobsons"
p [milo, lola]
# [#<Dog:0x007faf6115b158 @name="Milo", @family="The Jacobsons">, #<Dog:0x007faf6115b108 @name="Lola", @family="The Jacobsons">]

This works because Ruby gives you this syntactic sugar when you write a something= method, it lets you put a space before the = when calling the method. And that applies in this context too. Kind of neat.

order of operations

June 16, 2015

Last month, we looked at Ruby operators, and I complained about how I wish I could define my own operators. Today I’m looking at them a little more, and thinking about how Ruby handles expressions made up of multiple operations.

Let’s say you have this Ruby code:

sum = a + b + c

What are a, b, and c? They could be

  1. variables containing numbers; that’s what they kind of look like they want to be
  2. invocations of methods which return numbers
  3. variables or methods that contain/return absolutely anything else; let’s not worry about this
  4. maybe they’re not even defined at all; let’s not worry about this possibility either

Let’s look at how option 1 plays out:

a = 1
b = 1
c = 1
puts a + b + c
# 3

So far, so good.

Let’s see how option 2 plays out:

def a; 1; end
def b; 1; end
def c; 1; end
puts a + b + c
# 3

Sort of funky-looking, but also sort of straight-forward. Here’s the question though: if Ruby is calling those 3 methods to get those 3 values, what order are they being called in? Let’s find out:

def a
  puts "a"
  1
end

def b
  puts "b"
  1
end

def c
  puts "c"
  1
end

puts a + b + c
# a
# b
# c
# 3

It kind of makes sense. It’s just going from left to right, like English.

One cool thing about Ruby is that (almost) everything is an object, and even core things like math operations are implemented as methods. This means the above could be written like this:

puts a.+(b).+(c)
# a
# b
# c
# 3

This rendition makes it clear that this is a chained sequence of method calls. Let’s make it even more clear, by refining the plus method and adding some logging:

module MathLogger
  refine Fixnum do
    alias_method :original_plus, :+

    def +(other)
      original_plus(other).tap do |sum|
        puts "#{self} + #{other} = #{sum}"
      end
    end
  end
end

using MathLogger

def a
  puts "a"
  1
end

def b
  puts "b"
  1
end

def c
  puts "c"
  1
end

puts a.+(b).+(c)
# a
# b
# 1 + 1 = 2
# c
# 2 + 1 = 3
# 3

Now it’s not as simple as “left to right”. We start at the left and call the a method. But the next method we call is b, not +. Before we can add two values, we need to know what the values are, and Ruby will evaluate the expression in parentheses (here it’s calling a method, but it could be calling multiple methods and they would all be evaluated before the + method is called).


A brief digression about defined?

This rule doesn’t apply to the defined? method, which ships with Ruby and behaves like this:

msg = "Hello"
defined?(msg) #=> "local-variable"
OMG           #=> NameError: uninitialized constant OMG
defined?(OMG) #=> nil
OMG = 4
defined?(OMG) #=> "constant"

The third line of this excerpt demonstrates that referencing an uninitialized constant normally raises a name error, so it would be normal to expect the same to happen on the fourth line, because we just saw that Ruby normally evaluates the arguments to a method. Here it just totally doesn’t, which feels kind of weird and inconsistent. It might be helpful to think of defined? as a language keyword and not a method. See also the alias method.


Back to math. Remember PEMDAS? When evaluating an arithmetic expression, we’re not supposed to just read from left to right, evaluating operations as we go; we’re supposed to prioritize some operations above others:

  • Parentheses
  • Exponents
  • Multiplication
  • Division
  • Addition
  • Subtraction

With this acronym memorized, children are able to evaluate complicated math expressions.

Can Ruby? Let’s see:

4 + 3 * 5   #=> 19

Well… yeah! Seems right! But let’s take a look into the order that methods are being called:

module MathLogger
  refine Fixnum do
    alias_method :original_plus, :+
    alias_method :original_times, :*

    def +(other)
      original_plus(other).tap do |sum|
        puts "#{self} + #{other} = #{sum}"
      end
    end

    def *(other)
      original_times(other).tap do |product|
        puts "#{self} * #{other} = #{product}"
      end
    end
  end
end


using MathLogger

def four
  puts 4
  4
end

def three
  puts 3
  3
end

def five
  puts 5
  5
end

puts four + three * five
# 4
# 3
# 5
# 3 * 5 = 15
# 4 + 15 = 19
# 19

Interesting! So, Ruby takes a look at four, takes a look at three, and then skips the addition, then takes a look at five, and performs the multiplication. Only then does it double back and perform the addition, inlining the product of three and five.

That’s great! And surely, if all of these operations are just methods, it will behave the same when I change it to this?

puts four.+(three).*(five)
# 4
# 3
# 4 + 3 = 7
# 5
# 7 * 5 = 35
# 35

Hm, nope. When we call the methods directly, the order of operations breaks.

I always thought it was just “syntactic sugar” that I could omit the dot when calling the + method (and its siblings) but it’s doing slightly more than just inlining the dots: it’s also, more or less, inlining the parentheses, so it looks something like this:

puts four.+(three.*(five))

How does it choose where to put the parentheses? It has a precedence table which Ruby references when deciding which operations to evaluate before others. This means that if I were able to define my own operators, I would need to be able to insert them somewhere in this hierarchy, and this hierarchy would also be cluttered with all the operators added by the gems included in my project.

Naturally, my operator would be at the top of the list.