hardscrabble 🍫

By Max Jacobson

Psst. Check out my RubyConf 2017 talk, There are no rules in Ruby.

blog posts

Sad Blocks

07 Jul 2017

I wish Ruby knew when you wrote a sad block.

What is a sad block?

It’s something I just made up.

Consider this code:

Candle.all.each do |candle|
  puts candle.inspect
end

Let’s say you run it, and you see no output. What do you conclude? Probably that there aren’t any candles.

Well, maybe. Or maybe Candle is implemented like this:

# candle.rb
class DatabaseResult
  def each; end
end

class Candle
  def self.all
    DatabaseResult.new
  end
end

Look, that would be weird, but it’s possible, and Ruby doesn’t do anything to help you out here, and I feel like it should.

What’s happening? You’re calling the instance method #each of the DatabaseResult class. And it’s just not doing anything at all and doesn’t even know you gave it a block. Cool.

Brief digression time.

That method has an “arity” of zero. How do I know that?

$ irb
>> require "./candle"
=> true
>> DatabaseResult.instance_method(:each).arity
=> 0

Also by looking at it.

What does it mean? It means that the method takes zero arguments.

But when we count the arity, we’re not considering blocks, because blocks are a special, weird kind of argument, where you can provide it or not and it’s kind of outside of the method signature. You can have methods that takes a block and uses it, and its arity will still be zero:

# candle2.rb
class DatabaseResult
  def initialize(values)
    @values = values
  end

  def each
    @values.each do |value|
      yield(value)
    end
  end
end

class Candle
  def self.all
    DatabaseResult.new([
      new("Geranium"),
      new("Lavender"),
    ])
  end

  def initialize(scent)
    @scent = scent
  end
end

Candle.all.each do |candle|
  puts candle.inspect
end
$ irb
>> require "./candle2"
=> true
>> DatabaseResult.instance_method(:each).arity
=> 0

Even though we use the block we’re given, the arity is still zero.

What about that other syntax where you explicitly put the block in the method signature, does that make it count toward the arity?

def each(&block)
  @values.each do |value|
    block.call(value)
  end
end

I’ll tell you: it doesn’t. Even though it sort of feels like it should.

These versions of the method really require you to pass them a block, which you just have to know. If you forget to pass a block, you get a nasty error:

# candle4.rb
# ...
def each(&block)
  @values.each do |value|
    block.call(value)
  end
end
# ...

Candle.all.each
candle4.rb:9:in `block in each': undefined method `call' for nil:NilClass (NoMethodError)
        from candle4.rb:8:in `each'
        from candle4.rb:8:in `each'
        from candle4.rb:27:in `<main>'

Or in this version, an even better error:

# candle5.rb
# ...
def each
  @values.each do |value|
    yield value
  end
end
# ...

Candle.all.each
candle5.rb:9:in `block in each': no block given (yield) (LocalJumpError)
        from candle5.rb:8:in `each'
        from candle5.rb:8:in `each'
        from candle5.rb:27:in `<main>'

No block given. Local jump error. Sure. That’s Ruby trying to be helpful and I appreciate that.

Ruby helps you (by raising a helpful error) when you don’t provide a block, but you were supposed to. But it doesn’t help you when you do provide a block, and you weren’t supposed to.

Ruby’s like, yeah, sure, just provide a block wherever you want, this is a free country.

If you wanted to change this behavior in your code, and get helpful errors when your blocks are unexpectedly not invoked, you could do something like this:

class SadBlock
  def initialize(&block)
    @block = block
    @called = false
  end

  def verify
    raise 'hell' unless @called
  end

  def to_proc
    ->(*args) {
      @called = true
      @block.call(*args)
    }
  end
end

sad_block = SadBlock.new do |candle|
  puts candle.inspect
end

Candle.all.each(&sad_block)
sad_block.verify

I don’t think you should do this, but you could, and I kind of wish Ruby just did it automatically.

I know it’s an impractical request, because there are valid use-cases where you might pass a block to a method, and the method just assigns it to an instance variable without calling it, but it promises to call it later. But maybe Ruby could detect that somehow. I’m just thinking out loud here.

I’ve seen tests where assertions lived in blocks, and the blocks were never being called, so they weren’t actually asserting anything.

I’ve seen configuration being done via a DSL in a block, except the block wasn’t being called, so the defaults were being used.

I guess what I’m saying is it’s a little weird to me that blocks aren’t treated like ordinary arguments. If they were, you’d get an ArgumentError if you forgot to provide it or if you provided it and it wasn’t expected.

That’s what I want.

there are no rules in ruby

03 Jul 2017

Note: I’ve expanded on these ideas in a conference talk, which you can see here.


I recently learned about a feature of the Ruby programming language that has shaken me to my very core.

Consider this code:

# dog.rb
class Dog
  attr_reader :name

  def initialize(name)
    @name = name or raise ArgumentError
  end
end

def get_dog
  Dog.new("Milo")
end

thing = get_dog
if Dog === thing
  puts thing.name + " is a dog"
end

What happens when you run this code? Feel free to try.

But I’ll tell you.

$ ruby dog.rb
Milo is a dog

This code seems pretty resilient to unexpected runtime errors.

Looking at the code, it seems pretty reasonable to believe:

when we have an instance of Dog, we will be able to send it the message name and get back a String

Up is up. The sky is blue. We’re living in a society.

Well, ok, but we can’t actually assume that the value will be a String, because it doesn’t check that. If we change our definition of get_dog, things blow up:

def get_dog
  Dog.new(["Milo"])
end
$ ruby dog.rb
dog.rb:15:in `<main>': no implicit conversion of String into Array (TypeError)

But, OK, at least that error message is pretty good. This is user error. When we write thing.name + " is a dog", we’re expressing some amount of faith in ourselves that we expect a String, because values of other types don’t necessarily respond to a + method. This is a leap of faith that we’re all willing to make when we use Ruby. Other languages eliminate the need to make that leap of faith by checking types when you compile your code, but Ruby doesn’t do that.

And that’s fine.

So maybe our expectation should be:

when we have an instance of Dog, we will be able to send it the message name and get back a truthy value

And we’ll just remember to provide Strings. Maybe we’ll write a comment indicating the expected type of the parameter.

Well, what if get_dog looked like this:

def get_dog
  dog = Dog.new("Milo")
  def dog.name
    nil
  end
  dog
end

Maybe it just casually redefined the name method for that instance. Then your program crashes like this:

$ ruby dog.rb
dog.rb:19:in `<main>': undefined method `+' for nil:NilClass (NoMethodError)

Which… OK, who’s going to write code like that? Not me and no one I work with, for sure!

But where does that leave our statement of beliefs?

when we have an instance of Dog, we will be able to send it the message name

We can’t even say “and get back a value” because what if the override raises an error?

Perhaps you see where this is going…

Well, what if get_dog looked like this?

def get_dog
  dog = Dog.new("Milo")
  dog.instance_eval('undef :name')
  dog
end
$ ruby dog.rb
dog.rb:17:in `<main>': undefined method `name' for #<Dog:0x007fecc104a870 @name="Milo"> (NoMethodError)

Which, again, lol. You can just remove methods if you want to? Sure. No one is going to write this. I know.

(By the way, hat tip to Agis on Stack Overflow for sharing this trick. I figured it was possible but didn’t know how.)

OK so what can we say for sure?

How about this:

when we have an instance of Dog, it will have an instance variable @name defined

Wow that’s sad! How do we even check that? Maybe like this:

thing = get_dog
if Dog === thing
  puts thing.instance_variable_defined?("@name").inspect
  puts thing.instance_variable_get("@name").inspect
end
$ ruby dog.rb
true
"Milo"

OK great, have we reached the bottom?

No, because there are no rules in Ruby.

We can probably break this in many ways. Here’s one:

def get_dog
  dog = Dog.new("Milo")
  dog.remove_instance_variable("@name")
  dog
end

IMO this one is a bit pedestrian. Yeah, fine, you can just remove instance variables on random objects if you want to. Of course. My spirit is already broken, this isn’t meaningfully worse.

So let’s just try to say something that we don’t have to take back right away:

when we have an instance of Dog, the code in the initialize method must have run

Right? That has to be true. We’re living in a society, remember?

Nope:

def get_dog
  Dog.allocate
end

That results in this output:

$ ruby dog.rb
false
nil

What the hell is this?

This is the thing I mentioned at the beginning that I learned recently. When we create new objects in Ruby, we usually use the new class method. Notably, we don’t call the initialize instance method ourselves, although that’s what we are responsible for defining. Ruby handles calling that method for us. But before Ruby can call an instance method, it needs an instance, and that’s where allocate comes in. It just makes an instance of the class.

And you’re allowed to use it in your Ruby code, if you want to.

(Hat tip to John Crepezzi whose blog post explains this really well)

If you do, you get back a normal instance of your class in every way, except that the initialize method hasn’t run.

You can even call your own initialize method if you want to:

def get_dog
  dog = Dog.allocate
  dog.send(:initialize, "Milo")
  dog
end

We have to use send because initialize is private. Well, unless we change that:

class Dog
  attr_reader :name

  public def initialize(name)
    @name = name or raise ArgumentError
  end
end

def get_dog
  dog = Dog.allocate
  dog.initialize("Milo")
  dog
end

Sooooo where does that leave us?

when we have an instance of Dog, it’s a good dog

Basically: 🤷‍♂️.

That’s the bottom. That’s as far as I know how to go. Maybe there’s more. Please don’t tell me.


I want to emphasize: this is not a criticism of Ruby. I’m only faux-alarmed. Ruby is a springy ball of dough. It’s whatever you want it to be. All of these features are sharp knives you can use or abuse.

As I’ve been learning another language which feels much less pliant, I’ve started to notice things about Ruby that never occurred to me before. When I write Rust, I take some pleasure and comfort from the rigid rules. It’s more possible to use words like “guarantee” and “safety” in Rust-land.

But Ruby keeps you on your toes.