https://tech.stonecharioteer.com/posts/2025/ruby-blocks/ Stonecharioteer on Tech * Home * Resume * TIL * Tags * Search * Archive Home >> Posts Ruby Blocks How to start really getting Ruby, especially blocks. October 14, 2025 * 6 min [blocks-eve] Table of Contents * Block Magic + Building Your Own Language + Resource Management + Building a DSL + Custom Control Flow + Putting this all together I think I'm really starting to get Ruby. At least I can read Ruby and tell you what's happening. I haven't read a single tutorial on RSpec, the testing framework we use at Chatwoot for the Rails backend. I didn't want to spend too much time in tutorial hell, and I'm sure that I should read more about it soon. I've written about Ruby blocks before, but I think it bears repeating. This is a method call with a block as an input. 1 perform "input_value" do 2 puts "I'll get called with that function" 3 end Don't worry about how the function is implemented for now. I'm hand-waving this so that you can notice something. This is also a method call with a block as an input. 1 it("can do something") { puts "The cake is a lie" } But here's something else that's a method call with a block as an input. In lib/calculator.rb: 1 class Calculator 2 def add(a, b) 3 a + b 4 end 5 end In spec/calculator_spec.rb: 1 require_relative "../lib/calculator" 2 3 RSpec.describe Calculator do 4 it "adds two numbers" do 5 calc = Calculator.new 6 expect(calc.add(2, 3)).to eq(5) 7 end 8 end The highlighted lines include a method call to it with a block. Look at that again. It's a method call. That is crazy. It is so sublime that I can't explain how excited this makes me. If I have to teach Ruby to a Pythonista I'd ask them to ensure they see what this is. Until you grok this, it won't matter how much you try to understand Ruby. Ruby's readability comes from this feature. I'm still not sold on RSpec though, but I am open to learning it because it is, ultimately, Ruby. Block Magic# Let's really see what you can do with blocks. Building Your Own Language# 1 3.times do 2 puts "Hello" 3 end 4 5 5.times { puts "World" } 6 7 10.downto(1) { |i| puts i } These are all method calls on integers. times is a method. downto is a method. And every method takes blocks. But we can also take this to the monke. 1 class Integer 2 def seconds 3 self 4 end 5 def minutes 6 self * 60 7 end 8 def from_now 9 Time.now + self 10 end 11 end 12 13 meeting_time = 30.minutes.from_now 14 puts "Meeting is in 30 minutes: #{meeting_time}" That line is readable, not because Ruby is magically more readable than other languages, but because we're daisy-chaining methods we implemented on the built-in type. We gave Integer wings. We created a mini language that looks like it somehow isn't Ruby but, sweet God in Vaikuntha, it really is. Resource Management# In Python I'm used to doing this. 1 with open("data.txt", "w") as file: 2 file.write("This is some data\n") 3 file.write("This is some more data\n") But you'd be surprised how much production code is in the wild that doesn't use this. You should be writing it like this in Python, but with is a language keyword in Python. What would I do in Ruby? 1 File.open("data.txt", "w") do |file| 2 file.puts "This is some data" 3 file.puts "This is some more data" 4 end File.open is a regular old Ruby method. It takes block. THEY ALL TAKE BLOCKS. 1 def with_timer(name) 2 start_time = Time.now 3 yield 4 end_time = Time.now 5 puts "#{name} took #{end_time - start_time} seconds" 6 end 7 8 with_timer("Database query") do 9 # Some expensive operation 10 sleep(2) 11 end That just added a neat side-effect to the database operation. Building a DSL# Now let's really turn things up. 1 class TodoList 2 def initialize 3 @tasks = [] 4 end 5 6 def task(description, &block) 7 task_obj = Task.new(description) 8 task_obj.instance_eval(&block) if block 9 @tasks << task_obj 10 end 11 12 def show 13 @tasks.each { |t| puts t} 14 end 15 end 16 17 class Task 18 attr_accessor :priority, :due_date, :description 19 20 def initialize(description) 21 @description = description 22 end 23 24 def priority(level) 25 @priority = level 26 end 27 28 def due(date) 29 @due_date = date 30 end 31 32 def to_s 33 "#{@description} (Priority: #{@priority}, Due: #{@due_date})" 34 end 35 end 36 37 38 # let's use this 39 def todo(&block) 40 list = TodoList.new 41 list.instance_eval(&block) 42 list 43 end 44 45 my_todos = todo do 46 task "Write a blog post" do 47 priority "high" 48 due "2025-10-15" 49 end 50 51 task "Review PR" do 52 priority "medium" 53 due "2025-10-14" 54 end 55 end 56 57 my_todos.show Just stop reading and look at that syntax. I had a problem when I first tried learning Ruby in 2021. I had tried, because I was trying to just learn a new programming language (as one should definitely do annually), and I was a little gob-smacked because I didn't understand the config.rb file from Rails. It had a bunch of things like this. It looked like a damned configuration file. Django tries to do this with the settings.py file, but it doesn't succeed. This is exactly why Rails routing looks like this. 1 Rails.application.routes.draw do 2 resources :users do 3 member do 4 get :profile 5 post :activate 6 end 7 end 8 9 namespace :admin do 10 resources :posts 11 end 12 end That's not something special from Rails. That's just a bunch of Ruby methods taking blocks! The draw, resources, member, namespace, they're all just regular methods. All of this looked like some magical DSL to me. But it was not. It was just regular old Ruby. Custom Control Flow# Okay let's make our own unless or if syntax. Just because I'm feeling like it. 1 def only_on_weekdays 2 yield unless [0, 6].include?(Time.now.wday) 3 end 4 5 only_on_weekdays do 6 puts "Let's get to work!" 7 8 end Or maybe a retry function? 1 def with_retry(max_attempts: 3) 2 attempts = 0 3 begin 4 attempts += 1 5 yield 6 rescue StandardError => e 7 if attempts < max_attempts 8 puts "Attempt #{attempts} failed, retrying..." 9 retry 10 else 11 puts "Max attempts reached, giving up" 12 raise e 13 end 14 end 15 end 16 17 with_retry(max_attempts: 3) do 18 # some unreliable API 19 20 puts "Attempting connection..." 21 raise "Connection failed" if rand > 0.7 22 puts "Success!" 23 end Putting this all together# 1 (1..5).select { |n| n.even? } 2 .map { |n| n * 2 } 3 .reduce(0) { |sum, n| sum + n } Each and every one of those was a method call that took a block! It's all a block. Once I saw this pattern, I could not unsee it! And why would I want to unsee it? It's beautiful. It sparkles, damn it. The Real Magic The magic isn't just that you can do this. It's that Ruby's syntax makes it so natural that blocks become invisible. When you write 5.times { puts "Hello" }, you don't think "I'm calling the times method and passing it a block." You think "I'm doing something 5 times." You read RSpec code and think "Yeah, that reads like English." That's the genius of Ruby. The language gets out of your way and lets you think about the problem, not the syntax. * Ruby Next >> Returning from Ruby Blocks, Procs and Lambdas * * * * * * * (c) 2025 Stonecharioteer on Tech * Powered by Hugo & PaperMod