Lisp's Influence on Ruby
The Lisp Legacy within Ruby

Consider the following line of Ruby:
users.select { |u| u.admin? }
At first glance, the hallmarks of Lisp are missing: there are no nested parentheses and no prefix notation. Instead, we have syntactic blocks. However, the essence remains. The structural pattern—chaining a transformation to a filter and utilizing a predicate method (ending in ?) to ask a boolean question—is pure Lisp.
Matz has suggested that Ruby's architecture began as a basic Lisp, where he stripped out the macros and s-expressions and then integrated a Smalltalk-inspired object system, blocks, and methods. While many developers are drawn to Ruby for its Object-Oriented (OO) nature, the features that truly captivate "Rubyists" are often the ones inherited from Lisp.
🛠️ The Lisp-Inspired Toolkit
Here is a breakdown of the specific features Ruby borrowed from the Lisp tradition and why they are transformative.
1. Predicate Method Naming
In Ruby, it is standard for methods that return a boolean to end with a ?.
- The Benefit: It provides an immediate visual cue that the method is a predicate.
- The Influence: While a small syntactic choice, it permeates the entire ecosystem.
2. Closures and Blocks
Blocks are arguably the most beloved feature of the language. Technically, these are closures: segments of code that "capture" the environment in which they were created and can be passed around as first-class values.
total = 0
[1, 2, 3].each { |n| total += n }
# total is now 6
In the example above, the block closes over the total variable. This is the classic closure pattern: a function that remembers its birthplace.
Evolution of the Concept:
- Scheme: First to make closures first-class objects.
- Ruby Blocks: Closures with the parentheses removed (using
do...endor{}). - Procs/Lambdas: Closures with the parentheses restored.
The term "lambda" originates from Alonzo Church's lambda calculus, which was first implemented in a practical language in 1958. In Ruby, the arrow syntax represents this:
square = ->(n) { n * n }
[1, 2, 3].map(square) # => [1, 4, 9]
3. First-Class Functions
When a closure can be named and passed as an argument, functions effectively become values. They can be stored in arrays, returned by other methods, or attached to objects.
Ruby employs a bit of "magic" with symbols. For instance, :method_name can be converted into a block by looking up that method on the receiver:
emails = users.map(:email)
admins = users.select(:admin?)
This works because:
- The symbol
:foois coerced into aProc. - The
Procis passed as a block. - The block is executed for each element.
This mirrors Lisp's core philosophy: programs are constructed by composing functions. Ruby simply dresses this composition in "dot-chains."
4. The Power of Symbols
A symbol (like :status) looks like a string with a colon, but it is a fundamentally different value. Symbols are interned.
Comparison: Strings vs. Symbols
| Feature | Strings ("foo") | Symbols (:foo) |
|---|---|---|
| Memory | Usually separate objects | Always the same object |
| Comparison | Slower (character by character) | Fast (pointer comparison) |
| Hashing | Computed | Free/Pre-computed |
In Lisp, these are known as atoms. The reader identifies foo, checks a symbol table, and either retrieves the existing symbol or creates a new one.
Mathematically, we can think of symbol identity as:
:status.equal?(:status) # => true
"status".equal?("status") # => false
This enables efficient hash keys and is the engine behind Ruby's reflective metaprogramming.
5. Collection Methods (The Enumerable Module)
The Enumerable module is the heart of Ruby's expressiveness. Methods like map, select, reject, reduce, flat_map, zip, partition, and chunk_while follow a consistent Lisp-like shape:
.
orders.select { |o| o.placed_at > 1.week.ago }
.group_by(:customer_id)
.transform_values { |group| group.sum(:total) }
The Pipeline Flow:
"When Rubyists say 'the language reads like English,' what they usually mean is 'the collection methods compose into sentences.'"
6. Lazy Enumerators
Standard collection methods are "eager"—they compute the entire result immediately, allocating memory for the whole array. However, Lisp (and specifically Scheme's delay and force, or Clojure's lazy sequences) introduced the idea of deferred computation.
Enumerable#lazy creates a recipe rather than a result. It pipes operations together without creating intermediate arrays.
(1..Float::INFINITY).lazy
.select { |n| n % 3 == 0 }
.map { |n| n * n }
.first(5)
# => [9, 36, 81, 144, 225]
Without .lazy, the select method would attempt to process an infinite range, causing the program to hang. With it, values flow through the chain one by one.
7. Duck Typing
Finally, there is the concept of Duck Typing: "If it walks like a duck and quacks like a duck, treat it like a duck."
While Smalltalk's "message passing" is a more direct ancestor to Ruby's implementation, the broader tradition of dynamic typing—where the value carries the type information rather than the variable—is a cornerstone of the Lisp philosophy.
Written by Ian Johnson