hardscrabble 🍫

By Max Jacobson

Psst. Check out bluff.website, a game I made, for you to play with your friends on your zoom calls.

eighty character lines

05 Sep 2015

Last month we talked about RuboCop, which analyzes your Ruby code and nitpicks it. One of its most difficult to follow suggestions is to keep your lines of code no longer than 80 characters.

The creator of rubocop, bbatsov, explained his perspective on his blog:

We should definitely have a limit – that’s beyond any doubt. It’s common knowledge that humans read much faster vertically, than horizontally. Try to read the source code of something with 200-character lines and you’ll come to acknowledge that.

I’m totally on board with the short lines train. For me, it only gets tricky when dealing with nested stuff (examples to follow) which add a lot of space to the left of the first character of code. For example:

module MyGreatGem
  module SomeOtherNamespace
    module OmgAnotherNamespace
      module LolYeahOneMore
        class SomethingGreat
          class SomethingOk
            class MyGreatClass
              def initialize
                puts "OMG I only have 64 characters to express something on " \
                     "this line! And now it's more like 'these lines' haha"
              end
            end
          end
        end
      end
    end
  end
end

Often strings are the first thing to get chopped up, as in that example.

The only approach I thought of to deal with that is to organize my code differently to not use many nested namespaces. That’s probably not the worst idea, honestly, but I’m writing this post to share an interesting style I observed in the wild (read: on github) that takes a whole nother approach:

# Excerpted from:
# https://github.com/net-ssh/net-sftp/blob/ebf5d5380cc533b69b308baa2e396e4a18abc900/lib/net/sftp/operations/dir.rb
module Net; module SFTP; module Operations
  class Dir
    attr_reader :sftp

    def initialize(sftp)
      @sftp = sftp
    end
  end
end; end; end

Huh! That’s a style I hadn’t seen before. RuboCop has many complaints about it, and I don’t totally love the style, but it’s a very novel and neat way to do it, and it certainly frees up columns to spend on your code if you’re planning to stick to an 80 character limit.

One possible alternative is to define your namespaced class using this shorthand:

class Net::SFTP::Operations::Dir
  attr_reader :sftp

  def initialize(sftp)
    @sftp = sftp
  end
end

If you do that, you get 2 extra characters on each line. Sweet!

One problem: it sort of doesn’t work, at least not in the same way.

If you just look at that example, and imagine that you’re the Ruby interpreter trying to figure out what this code means, how are you supposed to know whether Net, SFTP, and Operations are supposed to be classes or modules? You have to already know by them being previously defined. If they haven’t been defined yet, you are well within your right to raise a RuntimeException to complain that this constant hasn’t been defined yet, rather than try to guess.

Both of the earlier longhand examples were explicitly explaining what the type of each namespace constant is. That pattern works whether you’re defining the module or class in that moment, or “opening” a previously defined module or class to add something new to it. This shorthand, while optimal for line length, only works when opening previously defined constants.

One downside of this approach is that, by relying on all of the namespaces being predefined, it becomes harder to test this class in isolation (it’s probably possible to do it through some gnarly stubbing but, harder). You’re also introducing some requirements about the order in which the files from your code need to be loaded, which feels kind of fragile.

One possible upside comes to mind. When you follow the typical pattern of writing out all the namespace modules and classes, you introduce some room for error: what if in one file you write class Operations by mistake (instead of module Operations)? You’ll get an error. That’s not too bad, honestly.

I think 80 is usually enough but if you’re doing too many contortions to stay in that box, try like 90 or 100, you’re still a good person.

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

09 Aug 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?