Hardscrabble 🍫

By Max Jacobson

Making FactoryBot.lint verbose

10 Feb 2021

Here’s a quick blog post about a specific thing (making FactoryBot.lint more verbose) but actually, secretly, about a more general thing (taking advantage of Ruby’s flexibility to bend the universe to your will). Let’s start with the specific thing and then come back around to the general thing.

If you use Rails, there’s a good chance you use FactoryBot to help you write your tests. The library enables you to define “factories” for the models in your system with sensible default values.

FactoryBot has a built-in linter, which you can run as part of your CI build. It will try to identify any factory definitions which are faulty.

At work, we run this as a Circle CI job. It almost always passes, but every now and then it catches something, so we keep it around.

Recently, it started failing occasionally, but not because of anything wrong with our factories. Instead, it was failing because it was just… slow. Circle CI is happy to run things like this for you, but it gets antsy when something is running for a while and printing no output. Is it stuck? Is it going to run forever? If something is running for 10 minutes with no output, Circle CI just kills it.

Our factory linter apparently takes ten minutes now, we learned.

So, what to do about that?

Per that support article, one option is to just bump up the timeout. That’s easy enough. We could tell Circle CI to wait 15 minutes. In a year or two, maybe we’ll need to bump it up again, assuming such pedestrian concerns continue to dog us then, while we drive around in our flying cars.

Another option would be to just stop running it. It’s useful-ish but not essential. That’s easy enough.

Another option would be to configure the linter to print verbose output while it’s running. If we could do that, then we’d get multiple benefits: first of all, Circle CI would be satisfied that it is not stuck, and that it is making progress, and that it might eventually finish, even if it takes more than ten minutes; but also, having some output might be interesting and useful, no? Hmm. I pulled up the FactoryBot docs and saw an option verbose: true, but it didn’t seem to be what I wanted:

Verbose linting will include full backtraces for each error, which can be helpful for debugging

I want it to print output even when there are no errors. I didn’t see anything like that.

Imagine a ruby file with this stuff in it:

require 'factory_bot'

class Dog
  attr_accessor :name

  def save!
    sleep 4
    true
  end
end

class Band
  attr_accessor :name

  def save!
    sleep 4
    true
  end
end

FactoryBot.define do
  factory :dog, class: Dog do
    name { 'Oiva' }
  end

  factory :band, class: Band do
    name { 'Bear Vs. Shark' }
    albums { 2 }
  end
end

factories = FactoryBot.factories
FactoryBot.lint(factories)

There’s actually a bug in our Band factory: it references an attribute called albums which does not exist in our model code. The linter will catch this.

Looking at that last line, it looks like we just pass in the list of factories, and then presumably it will loop over that list and check them one-by-one.

Looping over things is a really common thing in Ruby. Anything that you can loop over is considered “enumerable”. Arrays are enumerable. Hashes are enumerable. When you query a database and get back some number of rows, those are enumerable.

A list of factories is enumerable. Hmm.

Let’s try writing our own enumerable class, to wrap our list of factories. We’ll call it ChattyList. It’ll be a list, but when you loop over it, it’ll chatter away about each item as they go by.

In general, if you’re calling a method and passing in one enumerable thing, it would also be fine to pass in some other enumerable thing. It’s just going to call each on it, or reduce, or something like that from the Enumerable module.

class ChattyList
  include Enumerable

  def initialize(items, before_message:, after_message:, logger:)
    @items = items
    @before_message = before_message
    @after_message = after_message
    @logger = logger
  end

  def each
    @items.each do |item|
      @logger.info @before_message.call(item)
      yield item
      @logger.info @after_message.call(item)
    end
  end
end

factories = ChattyList.new(
  FactoryBot.factories,
  logger: Logger.new($stdout),
  before_message: -> (factory) { "Linting #{factory.name}" },
  after_message: -> (factory) { "Linted #{factory.name}" },
)

FactoryBot.lint(factories)

When I run the script, the output looks like this:

I, [2021-02-10T22:43:03.676359 #57462]  INFO -- : Linting dog
I, [2021-02-10T22:43:07.678616 #57462]  INFO -- : Linted dog
I, [2021-02-10T22:43:07.678712 #57462]  INFO -- : Linting band
I, [2021-02-10T22:43:07.679373 #57462]  INFO -- : Linted band
Traceback (most recent call last):
        2: from app.rb:59:in `<main>'
        1: from /Users/max.jacobson/.gem/ruby/2.6.6/gems/factory_bot-6.1.0/lib/factory_bot.rb:70:in `lint'
/Users/max.jacobson/.gem/ruby/2.6.6/gems/factory_bot-6.1.0/lib/factory_bot/linter.rb:13:in `lint!': The following factories are invalid: (FactoryBot::InvalidFactoryError)

* band - undefined method `albums=' for #<Band:0x00007ff7b9022890 @name="Bear Vs. Shark"> (NoMethodError)

Nice! As the linter chugs thru the factories, it prints out its progress. With this, Circle CI will see that progress is happening and won’t decide to kill the job. This option wasn’t offered by the library, but that doesn’t have to stop us. Isn’t that fun?

By the way: that might be a good option to add to FactoryBot! Feel free, if you’re reading this, to take that idea.

Hardscrabble #2

08 Feb 2021

Note: this was originally posted on a Substack newsletter that I abandoned after two issues. Here’s the second one, converted to a regular blog post.

Hey! Thanks to everyone who read Hardscrabble #1 in July. I have to acknowledge: I said it would be a weekly thing and then let it sit for like seven months without sending a second email. I think I’ve learned some lessons about expectations-setting, because this introduction is about to end.

Today, as usual, I have a story from my life, an observation from work, a cultural recommendation, and some hyperlinks. Hope you’re taking care of yourself.

A story from my life

Will at a body building competition

In July last year, I called up a bodybuilder named Will, who I follow on Instagram, and chatted for about an hour. He was at his day job, where he does graphic design for a newspaper. A few days earlier, he had competed in his first event, The Missouri State Championships, and won the novice division. What was it like training and competing for that during a pandemic, I wondered.

I also had a bit of an ulterior motive. Will and I crossed paths on the internet almost ten years ago, and became Internet Friends. We shared a common interest: Apple gadgets, and the loose network of bloggers and podcasters who talked about them. Both of us had set up personal blogs modeled after Daring Fireball, which meant that they were mostly short posts linking to other blogs with our commentary tacked on. This was the thing to do at the time, for a particular kind of nerd, resulting in a shaggy, growing constellation of blogs which kept my Google Reader account full of takes each day.

Will and I gradually fell out of touch as we outgrew our shared interest, but we still followed each other on social media. I’m sure you have friendships like this, where you don’t talk to each other, but you sporadically see each other’s updates, when the algorithms happen to allow it. People’s lives move in fast forward when you aren’t paying attention to them.

Will physically transformed in a pretty dramatic way. I wondered whether I had transformed, too, from his perspective. I spent those years focusing on establishing a career in tech. Like his thing, it required a lot of dedication. Is that the same?

I’ll share a few excerpts from our conversation, lightly edited for clarity.


Will

Most of the people who follow me [on Instagram], up until recently, weren’t bodybuilding or fitness people, they were just like, normal people, so they get a peek into this weird, crazy lifestyle.

Hardscrabble

Yeah, that’s sort of where my fascination came from. There’s this feeling of like, could I do that? If I actually dedicated, you know, five-plus years —

Will

Literally, anybody can.I feel like with this sport, you can super nerd out in it. There’s science behind the training, science behind all the nutrition. You can really nerd out on every single detail. You just get lost in YouTube videos about proper training, proper diets, why you eat certain foods, what is effective. Like, bro science versus real science. Like, bro science is kind of what works and what people have done. Real science says this, but bro science says “Yeah, but if you do this, it works.”

Hardscrabble

Wait, what are you saying?

Will

You can’t always listen to the science, sometimes you’ve got to listen to your body. “For my body, we tried it the science way, and it didn’t really work. I’m gonna try it the way this huge bodybuilder dude told me he did it.”


To be clear, I don’t personally want to become a bodybuilder. But I like believing that I could, if I wanted to, according to someone who’s done it. Transformative change usually feels impossible, but it happens all the time. It just requires commitment:

Will

It’s such a weird lifestyle. No joke. You’ve got to be partially insane, obsessed to want to do this. It’s fitness, but to such an extreme that, to a normal person, it’s hard to understand. My mom is like, “Why are you doing this?” Because I’m, like, suffering while they’re all enjoying this great food. They came to visit when I was two weeks away from the competition. They’re having a birthday party, celebrating, pizzas, I’m eating these boring chicken and rice and broccoli meals. And it’s like “Ah, it’ll be worth it…“

And it takes time:

Will

I set out to do this two years ago. The goal was to compete in a competition, and then after after a year it was, I want to win one. I have no problem waiting. That’s the thing about bodybuilding, is it takes a long time. When I first started, I knew that in ten years I would probably be where I want to be. That’s how long this thing takes. And I’m six years into it. I’m like halfway from my original ten year idea. I have no problem, if I want to do this, but it might take two years. And then a year later, it’s “in a year, I think I’ll be ready”. And even now, I probably could’ve waited another year. But I was just itching to do it.

Hardscrabble

I think that’s what a lot of people struggle with, with fitness, is the long term nature of it. This is something that I can speak to from experience, the fact that it takes time is partly what gives you that sense of accomplishment, but it’s also what can be very frustrating about it if you don’t have that perspective that you expect it to take that long.

Will

Yeah. I always thought, five years is going to pass. I’m going to be forty eventually. So I can either do this, and then at forty I’ll be really happy with where I’m at or I can not do it and I’ll be at the same exact place that I’m at.


The big question, of course, is: why?

Will

At the end of the day, if you did everything perfect, it makes you feel superhuman. It’s like, wow, I was able to mentally make my body do things it didn’t want to do, turn down things it wanted, only eat the foods I was supposed to eat, and do everything you’re supposed to do. And you do that for weeks on end, and it makes you feel like you have incredible control over yourself. It makes you feel powerful.


For Will, the milestone he was working toward was his first competition. Perhaps you already know what happens at a body building competition, but I didn’t. I asked if it’s like, who can lift the most?

Will

No, that would be a powerlifting competition. There’s nothing physical. You’re judged on your muscular size, symmetry and proportion, and then “conditioning” which is how lean you are. Your hamstrings and your glute muscles only start to show when you’re really, really, really lean. They judge everybody standing next to each other, and then you have a couple hour break, and we all come back, and then we did our little routine to music.

The routine is about a minute long. In it, Will cycles through almost twenty poses, flexing various muscles, wearing nothing but his glasses and some bikini bottoms (he graciously credits Kitty’s Bikinis in the caption). I think I’d feel a bit exposed, but he looks very comfortable, like it’s the most natural thing in the world to be doing. Like dance, it has both movement and stillness. Will tells me his posing role model is Terrence Ruffin whose work is high art. I have to agree.

Here’s how Will felt onstage:

Will

My heart was racing onstage. I look like a deer in headlights in some of my pictures because I was just so nervous. You want to be able to flex every muscle in your body, but have a completely relaxed face, smiling, looking great, and I just didn’t have that stage confidence for my first time. I was just, heart pounding, trying to listen to my coach who’s yelling at me, telling me what to do.

Hardscrabble

And you’re wearing glasses.

Will

I hate contacts. And I feel like that’s just part of my look. I wasn’t even sure if you were allowed to wear glasses onstage. I asked my coach, _are they going to make me take these off?

Will was competing in the novice division, which is anyone who has never competed before. He won. He also competed in the open, and came in second. Why not first?

Will

He just had more muscles, so he won. I just have another year or two and then I’ll be able to compete with people. Progress is measured in years.

It’s heartening to me that bodybuilders are also self-conscious about how they look. We talked a bit about Instagram:

Will

I had quite a few unfollow. Which I don’t mind. I’m posting pics half naked, so if you don’t want to randomly see that on your feed, like people from high school, they’re not into it.

Hardscrabble

One of the reasons I can imagine a guy from high school might unfollow is maybe a bit of homophobia, like “Uh oh, not for me! I can’t look at that!”

Will

Maybe? Maybe. I think it’s more like, they think I’m way too into myself. If I post a crazy picture I try to have a funny caption, or poke fun at myself. But I think people can just be like, “Oh, he’s so into himself.” When really, I post a picture, and I’m like, “Ehh, I wish this was like this, or this is no good, this picture is terrible.”

He tells me about the process of taking a bodybuilding photo for Instagram, most of which are taken by his girlfriend.

Will

It takes hundreds of tries to get the right one. I’m making a dumb face or, to get the portrait mode popping just right, all the things. We’ll take like ten different things that are the same exact pose and it’s just, “All right, let’s try it again.” And if you watch that, it’s like, “Who are these losers?”


It was a good conversation. We covered a lot more, but honestly it’s kind of hard to transcribe and summarize interviews, and I feel like you get the gist. I’ll leave you with this.

Finally, I asked Will about the tech blogs. Do you remember all those characters from that scene? Do you remember that one anonymous blogger who just razzed everybody? Who do you think that was?

Will

I still read Daring Fireball, and that is the only one. I don’t listen to any of the podcasts.

An observation from work

I wrote a few blog posts about coding in the last few weeks:

  1. A little trick that makes using ripgrep in visual studio code nicer
  2. sindresorhus/pure is such a good zsh prompt
  3. heredocs in ruby

A cultural recommendation

In December, I wrote a blog post all about Zoey’s Extraordinary Playlist, the NBC musical dramedy that just started up its second season. I really, really love it, to a corny extent.

Some hyperlinks

  • Thanks Will for doing the interview. Make sure to follow his very entertaining instagram: @kujawawa.
  • My friend Alex Liu, a product manager at Verily Health, launched a newsletter called Refactored Health about “digital health strategy”. Seeing his announcement also inspired me to try writing this, and I regret not mentioning that in the list last week. I’m already learning a lot about our modern healthcare industry from his first few posts.
  • Tom Scocca argues against using brackets to clean up quotations when quoting somebody. Too late, Tom! But I’ll keep it in mind next time.
  • Giri Nathan’s writing about tennis, such as this coverage of Denis Shapovalov’s match from this morning, which mostly focuses on a tantrum the Canadian threw between games, is so fun to read.

heredocs in ruby

03 Feb 2021

I’ve recently been writing a lot of heredocs in Ruby. We have to talk about it.

what is the deal with heredocs?

It’s one of the ways to make a string. It looks like this:

def help
  <<TEXT
Help.
I need somebody
TEXT
end

help # => "Help.\nI need somebody\n"

The idea is that you have some all caps label on the first line (TEXT in that example), and then Ruby will look at the next line as the start of the string, and keep going until it sees that label again, and then the string is over.

It’s pretty similar to just using quotation marks like usual:

def help
  "Help.
I need somebody
"
end

help # => "Help.\nI need somebody\n"

One nice thing about the heredoc syntax is that you can use quotation marks in the middle of the string, and you don’t need to worry that you’re accidentally going to close the string.

That’s a pretty standard one, but there are a bunch of variations on the theme.

For example, this one is more common in my experience:

def help
  <<-TEXT
Help.
I need somebody
  TEXT
end

help # => "Help.\nI need somebody\n"

It looks a little nicer to indent the closing TEXT at the same level as the starting one, but that’s not allowed with standard heredocs. If you want to do that, you need to start the heredoc with <<- instead of <<.

It would look even nicer if you could indent the text of the string itself. Unfortunately, if you do that, it affects the actual value of the string:

def help
  <<-TEXT
    Help.
    I need somebody
  TEXT
end

help # => "    Help.\n    I need somebody\n"

No worries – they thought of that. You can use the “squiggly heredoc” syntax, which lets you write it like that without actually affecting the value of the string:

def help
  <<~TEXT
    Help.
    I need somebody
  TEXT
end

help # => "Help.\nI need somebody\n"

Most of the time, you should use a squiggly heredoc.

There is one last variation, which I’ve never seen in production code, but which I’ll share for completeness’s sake. This is the single-quote heredoc:

def help
  <<~'TEXT'
    Help.
    I need #{somebody}
  TEXT
end

help # => "Help.\nI need \#{somebody}\n"

When you put single quotes around TEXT – our heredoc delimiter in these examples – Ruby will treat the string like a single-quoted string rather than a double-quoted string. You know how in Ruby, if you want to use interpolation, you need to use double quotes?

"#{1 + 1}" # => "2"
'#{1 + 1}' # => "\#{1 + 1}"

Well, someday you’ll want to create a heredoc which behaves like a single quoted string (I don’t know why, to be honest) and you’ll be glad that you can.

why have I been writing so many heredocs recently?

At work, we use Rubocop to format our code. One of its rules, Layout/LineLength, checks that your lines aren’t longer than 120 characters. I think it’s a pretty good rule, and I’m gradually updating the existing code to follow it.

For the most part, it’s pretty straight-forward. Maybe you have a line that looks like:

foo(:a, :b, :c)

And you change it to

foo(
  :a,
  :b,
  :c,
)

Great, now it’s growing vertically instead of horizontally.

But what about lines that look like:

Rails.logger.info "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."

What do you do with that?

Let’s throw a heredoc on it:

Rails.logger.info <<~MSG.chomp
  Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
MSG

It’s still quite long, but the Rubocop rule has a loophole: heredocs are fine. You can disable this loophole via configuration, but I don’t want to, I like it. As a reader, I know that when I’m looking at a heredoc, the whole thing is a string; even if part of it’s off screen, I’m not missing much, it’s just more string.

If we wanted to disable the loophole, we might write that as:

Rails.logger.info <<~MSG.squish
  Lorem ipsum dolor sit amet,
  consectetur adipiscing elit,
  sed do eiusmod tempor incididunt
  ut labore et dolore magna aliqua.
  Ut enim ad minim veniam, quis
  nostrud exercitation ullamco
  laboris nisi ut aliquip ex ea
  commodo consequat. Duis aute
  irure dolor in reprehenderit in
  voluptate velit esse cillum dolore
  eu fugiat nulla pariatur. Excepteur
  sint occaecat cupidatat non proident,
  sunt in culpa qui officia deserunt
  mollit anim id est laborum.
MSG

That uses the String#squish method in Rails, which squishes a multi-line string onto one line. Is that better? That’s between you and your God. I can go either way.

One unexpected benefit of using heredocs

Imagine this code:

def my_great_query
  "select count(*) from users"
end

my_great_query # => "select count(*) from users"

When you’re editing that code in your text editor, your editor is using syntax highlighting to help you. Maybe it’s turning all of the keywords orange, or all of the method names blue. This can help your eyes to scan thru the code, and can help alert you to syntax errors. But that string on the second line is just a string, and it looks like all of the other string literals. Your editor does not know that it is a fragment of SQL, and that it can apply its SQL syntax highlighting to the contents of that string. How could it know that?

Well, imagine if you wrote it like this instead:

def my_great_query
  <<~SQL.chomp
    select count(*) from users
  SQL
end

my_great_query # => "select count(*) from users"

Now your editor has a context clue it can use, and perhaps it will elect to apply SQL highlighting to that string. VS Code, for one, does. I only use VS Code sometimes, but it’s things like this that give me a little pop of delight and make me want to make it more of a habit.

some of the quirks of using heredocs

One thing that really must be said before putting a bow on this blog post is that heredocs are kind of … weird. Like, what if you want to call a method on a heredoc, like to reverse it? It kind of feels like you should put that .reverse all the way at the end, like you would for a normal string:

# this is invalid
def help
  <<-TEXT
    Help.
    I need somebody
  TEXT.reverse
end

help

Why is this invalid? Well, remember what I said at the beginning of this blog post

Ruby will look at the next line as the start of the string, and keep going until it sees that label again, and then the string is over

(Yes I’m quoting this blog post in this blog post. I’m pretty sure that’s allowed.)

I could have been more clear there but I didn’t want to be so clear that it was confusing: the string ends when Ruby sees a line that has that label and nothing else. If it sees TEXT.reverse, that does not satisfy that rule.

So you need to write it like:

def help
  <<-TEXT.reverse
    Help.
    I need somebody
  TEXT
end

help # => "\nydobemos deen I    \n.pleH    "

One last quirk, via my colleague Ian. What if you want to start two heredocs on the same line? You probably shouldn’t, but it is possible:

def help
  [<<~TEXT, <<~SQL]
    Help.
    I need somebody.
  TEXT
    select * from lyrics
  SQL
end

help # => ["Help.\nI need somebody.\n", "select * from lyrics\n"]

Whoa.