I just completed the updated Ruby Koans (still the classic edgecase repo, with only minor tweaks needed for modern Ruby), and I’m genuinely impressed by how well they’ve aged. The core experience is almost unchanged, and it delivers very satisfying progression. There is real intent in where you start, all the way to the end. I picked up a ton of idiomatic Ruby tricks and deeper language intuition along the way, more than I was anticipating.
One small note: if you’re new to object-oriented programming and Ruby, the Koans might feel a bit sparse on explanation. They seem designed for someone who already has a solid mid-level grasp of OOP concepts (inheritance, message passing, etc.). Beginners might want to pair them with a more introductory resource first.
The format, though? Still brilliant. The red/green reflect cycle is kinda addictive and forces you to truly understand each concept before moving on. Test-driven enlightenment, you could say 😉
Running the Koans Without Touching Your Host Machine
I didn’t want to install Ruby locally on my Windows machine 🤡, so I spun them up in a VS Code dev container. It was super straightforward and portable, too!
Here’s the minimal devcontainer.json I used:
{
"name": "Ruby Koans",
"image": "mcr.microsoft.com/vscode/devcontainers/ruby:3.3",
"postCreateCommand": "gem install rake --no-document"
}
Debugging the Koans
Since I spend most of my days in .NET land and Visual Studio, I’m hooked on that excellent debugger. Thankfully, pry-byebug offers a very similar experience to Ruby, with breakpoints and inspection all in the terminal.
Top 12
The process of fixing one failing test at a time forces you to really understand why things work the way they do. This is where I really identified the intent behind the progression. It kept me honest; I didn’t want to progress until I felt I had completely grasped the concept. So, here are the bits I enjoyed the most (and things I learned):
1. Symbols are immutable, immortal, and definitely not strings.
Symbols are unique singletons that live forever (during the execution lifecycle in memory) and can't be changed. Great for hash keys and method names. The concept is relayed through the comparison of object_id.
Singletons are also an important concept I am familiarizing myself with post-koans. Basically, if your class represents something, like a single instance of a log file or connection (you only have one of), it should be a singleton. If it does not, no singleton.
:hello.object_id # => same every time
:hello.object_id # => identical to above
"hello".object_id # => different each time
"hello".object_id # => different again
:hello << "world" # => NoMethodError (symbols are immutable)
2. Small integers are immediate values with predictable object IDs.
This is just interesting, but turns out Ruby embeds small integers directly in the pointer, so their object_id is deterministic with this equation: 2 * value + 1. Looks like Fibonacci at first! 😵💫
1.object_id # => 3 (2*1 + 1)
5.object_id # => 11 (2*5 + 1)
100.object_id # => 201 (2*100 + 1)
3. Go for obj.nil? over obj == nil.
It's more idiomatic and clearly expresses intent. Also nil? is safe and cannot be overridden. It’s possible to override operators like == in a class and therefore screw up the assumed behaviour!
value = nil
if value.nil? # preferred
puts "it's nil"
end
if value == nil # works, but less clear
puts "it's nil"
end
4. Know when to use Hash#[] vs Hash#fetch.
[] silently returns nil for missing keys; fetch demands you handle the absence. So in other words, hash[key] returns nil when the key is missing (or the default if set). fetch lets you provide a fallback value or even a block! This is super useful for avoiding NoMethodError on nil.
h = { name: "Davis" }
h[:name] # => "Davis"
h[:age] # => nil (quiet)
h.fetch(:name) # => "Davis"
h.fetch(:age) # => KeyError
h.fetch(:age, 30) # => 30 (default value)
h.fetch(:age) { |k| 25 } # => 25 (block default)
5. Class constants behave differently in lexical vs inheritance scope.
Constants are lexically scoped when you look them up inside the class definition, but inheritance still works normally. Lexical scope lets nested classes see (and shadow) outer constants even though the outer module isn’t in the inheritance chain. But that lexical visibility does not propagate to external subclasses. To make a constant reliably available to subclasses, define it directly in the superclass (or use a fully-qualified name like ::Outer::CONSTANT to reopen).
Scenario 1: Relying on lexical lookup only
module Outer
CONSTANT = "Hello from Outer"
class Inner
def self.value
CONSTANT
end
end
end
class Subclass < Outer::Inner; end
Outer::Inner.value # => "Hello from Outer" (lexical fallback through nesting)
Subclass.value # => NameError (no access to Outer’s constant via inheritance)
Scenario 2: Explicitly defining (shadowing) the constant in the nested class
module Outer
CONSTANT = "Hello from Outer"
class Inner
CONSTANT = "Hello from Inner" # now defined on Inner itself
def self.value
CONSTANT
end
end
end
class Subclass < Outer::Inner; end
Outer::Inner.value # => "Hello from Inner"
Subclass.value # => "Hello from Inner" (inherited via normal hierarchy)
6. Regex shortcuts
I’ve written a bit already about using regex in C# here, but I did want to shout out The Koans’ very well constructed walk through of Ruby regex with great examples. Here’s a quick map:
- a
?means optional,+means one or more, and*means 0 or more \dis a substitute for[0123456789]- You can define a char range as
[0-9]instead of[0123456789] \sis a shortcut for whitespace chars\wis a substitute for the word char class[a-zA-Z0-9_].is a shortcut for and non-newline char\n. Ex:assert_equal "abc", "abc\n123"[/a.+/]^negates a char class. Ex:assert_equal "the number", "the number is 42"[/[^0-9]+/]Capital char class negates as well.
rubyassert_equal "the number is ", "the number is 42"[/\D+/] assert_equal "space:", "space: \t\n"[/\S+/] # ... a programmer would most likely do assert_equal " = ", "variable_1 = 42"[/[^a-zA-Z0-9_]+/] assert_equal " = ", "variable_1 = 42"[/\W+/]\Aanchors start of string.\zanchors end of string()group contents. Ex:assert_equal "hahaha", "ahahaha"[/(ha)+/]
"abc123"[/\d+/] # => "123" (\d = digit)
"hello world"[/\s+/] # => " " (\s = whitespace)
"var_1"[/\w+/] # => "var_1" (\w = word char)
"the number 42"[/\D+/] # => "the number " (\D = non-digit)
"hello\nworld"[/./] # => "h" (. = any except \n)
"ahahaha"[/ (ha)+ /] # => "hahaha" (group + repetition)
"line1\nline2"[/^line.+/] # => "line1" (^ = start of line in default mode)
"line1\nline2"[/\Aline.+/] # => nil (\A = true string start)
7. .inject is Ruby’s version for .reduce.
[1, 2, 3, 4].inject(0) { |sum, n| sum + n } # => 10
[1, 2, 3, 4].inject(:*) # => 24
Note:
:is the shorthand operator for[1, 2, 3].inject { |acc, el| acc.*(el) }giving you1 * 2 * 3 * 4. You could also use:+,:-,:/or even custom methods. Whoa!
8. Attr methods save you from boilerplate.
Honestly, since you have to deliberately write { get; set; } in C#, I always forget about attr in Ruby. But it does beat writing our boilerplate just to read an attribute!
class Person
attr_accessor :name, :age # getter + setter
# equivalent to:
# def name; @name; end
# def name=(val); @name = val; end
# def age; @age; end
# def age=(val); @age = val; end
end
p = Person.new
p.name = "Davis"
p.name # => "Davis"
9. super intelligently forwards arguments to the parent.
I love this, it’s one of the idiomatic beauties of Ruby. Just remember what your return type is!
class Parent
def greet(name)
"Hello, #{name}"
end
end
class Child < Parent
def greet(name)
super + " from Child!" # automatically passes name
end
end
Child.new.greet("Davis") # => "Hello, Davis from Child!"
10. There are many ways to define class methods
This singleton syntax is something I’ve never seen before, but it allows for concise grouping when defining multiple class methods. We love the shovel operator ❤️
class Dog
class << self
def bark
"woof!"
end
def species
"Canis familiaris"
end
end
end
Dog.bark # => "woof!"
Dog.species # => "Canis familiaris"
11. __send__ bypasses visibility restrictions.
Why does __send__ have two underscores? Consider how often you might use that in a class, and potentially override it! What if you had a Message class? You’ll definetely have a local .send method I’d wager. Also, I had no idea you could deliberately ignore simple access modifiers like this in Ruby.
class Secret
private
def hidden
"shhh"
end
end
s = Secret.new
s.send(:hidden) # => MethodError (respects private)
s.__send__(:hidden) # => "shhh" (ignores visibility)
12. The Proxy object project is dope!
One of the final “projects” you complete is fascinating! You are tasked with updating a predefined (but empty) Proxy class that should be able to initialize the proxy object with any object. Any messages sent to the proxy object should be forwarded to the target object. As each message is sent, the proxy should record the name of the method. This one is brain-bendy, but one of my favorite parts since I think it does an excellent job of tying in concepts previously used throughout the koans (like __send__)!
Final thoughts
This post is a bit longer than I anticipated, but I could’ve easily done my top 20 from the Ruby koans! Overall, I highly recommend giving them a pass if you’re curious at all and have a good Ruby baseline already. I will definitely be taking a swing at the C# koans soon. I’ll share my thoughts on those, too, if I like them!