Lisp's Influence on Ruby

Hacker News Top News

Summary

A technical blog post exploring how Ruby's design, including closures, first-class functions, symbols, and method naming conventions, was influenced by Lisp.

No content available
Original Article
View Cached Full Text

Cached at: 06/15/26, 12:58 AM

# Lisp’s Influence on Ruby Source: [https://blog.tacoda.dev/lisps-influence-on-ruby-6a54f1a7740e?gi=0730addad8f2](https://blog.tacoda.dev/lisps-influence-on-ruby-6a54f1a7740e?gi=0730addad8f2) [![Ian Johnson](https://miro.medium.com/v2/resize:fill:64:64/1*4-iVBjIv8Qk71ZP3EUDs4w.png)](https://blog.tacoda.dev/?source=post_page---byline--6a54f1a7740e---------------------------------------) Once I wrote`users\.select \{ \|u\| u\.admin? \}\.map\(&:email\)`and realized I’d written Lisp\. Not literally\. The parentheses are gone, the prefix notation is gone, the lambdas are syntactic blocks\. But the shape of the code \(chain a filter onto a transform, ask each element a yes\-or\-no question with`?`, build the result without mutating anything\) is Lisp\. Ruby just put it in business casual\. Matz has said as much\. He’s described Ruby’s design as starting from a simple Lisp, stripping out macros and s\-expressions, then adding an object system, blocks, and Smalltalk\-style methods\. The features most Rubyists fall in love with aren’t the object\-oriented ones\. They’re the functional ones, dressed in friendlier clothes\. Here is the list I think about often, and why each one matters\. ## Method names with question marks The convention that predicates end in`?`came from Scheme\.`zero?`,`nil?`,`empty?`,`respond\_to?`,`valid?`\. The mark tells you, at a glance, that the method answers a yes\-or\-no question\. It does not mutate\. It does not perform an action\. It tells you something true or false about the receiver\. ``` return if user.nil?return unless user.admin?notify(user) if user.subscribed? ``` You can read those three lines as English because the`?`\`?\` does the heavy lifting\. The same convention shows up as`\!`for methods that mutate or raise:`save\!`,`sort\!`,`compact\!`\. Both marks come from Scheme, where`null?`,`pair?`, and`set\!`work the same way\. A small syntactic borrow, but it threads through the whole language\. Reading Ruby is faster because of those two characters\. ## Closures and blocks Blocks are the feature most Rubyists name first when asked what they love about the language\. They’re closures: chunks of code that capture their surrounding scope and can be passed around as values\. ``` total = 0[1, 2, 3].each { |n| total += n }total # => 6 ``` The block closes over`total`\. That is the closure pattern: a function value that remembers the environment it was defined in\. Lisp had closures decades before Ruby\. Scheme made them first\-class objects you could pass to anything\. Ruby kept the idea and added the lighter syntax\. A block, with`do…end`or curly braces, is a closure with the parentheses stripped off\. Procs and lambdas are the same idea with the parentheses back on: ``` square = ->(n) { n * n }[1, 2, 3].map(&square) # => [1, 4, 9] ``` That arrow syntax is Ruby’s`lambda`\. The word itself is Lisp’s, from Church’s lambda calculus, plumbed into a working programming language for the first time in 1958\. ## First\-class functions Once you can name a closure and pass it around, functions become values\. You can store them in arrays, return them from methods, attach them to objects\. Ruby’s`Method`and`Proc`classes make this explicit\. So does`&:method\_name`, which converts a symbol into a block by looking up the method on the receiver\. ``` emails = users.map(&:email)admins = users.select(&:admin?) ``` That`&:foo`is a small piece of magic, and it works because functions are values in Ruby\. The symbol gets coerced into a proc, the proc gets passed as a block, the block gets called on each element\. First\-class functions all the way down\. This is Lisp’s foundational idea: programs are built by composing functions\. Ruby borrows the composition and dresses it up in dot\-chains\. ## Symbols `:foo`is a symbol\. It looks like a string with a colon, but it’s a different kind of value\. Symbols are interned: every time you write`:foo`, you get the same object\. Two strings that look the same are usually two separate objects in memory; two symbols that look the same are always one\. That property comes from Lisp\. Lisp symbols \(atoms, in some dialects\) are the original interned values\. The reader sees`foo`, looks it up in a symbol table, and either returns the existing symbol or creates a new one and remembers it\. After that, all references to`foo`point to the same object\. ``` :status.equal?(:status) # => true"status".equal?("status") # => false ``` What it buys you in Ruby: fast comparison, free hashing, and a clean syntax for names that aren’t strings\. ``` config = { host: "localhost", port: 5432, ssl: true }config[:host] ``` Hash keys are the obvious case, but the deeper use is method names\.`method\_name`and`:method\_name`are the same idea at two levels\.`send\(:save\)`calls the`save`method\.`define\_method\(:fetch\) \{…\}`defines one\.`respond\_to?\(:to\_s\)`asks if one exists\. Symbols are how Ruby refers to methods reflectively, which is how the metaprogramming works\. The`&:foo`shortcut from the last section is the same idea on a closer pass: a symbol naming a method, coerced into a callable\. Symbols carry the names; Ruby looks them up\. ## Collection methods `map`,`select`,`reject`,`reduce`,`each`,`flat\_map`,`zip`,`partition`,`chunk\_while`\. The`Enumerable`module is the part of Ruby I would miss most if I had to leave\. It’s also the part most directly descended from Lisp\. Lisp gave us`mapcar`,`filter`,`reduce`\. The shape is the same: take a collection, apply a function, get a collection back\. No indices\. No off\-by\-ones\. No accumulator variable to forget to reset\. ``` orders .select { |o| o.placed_at > 1.week.ago } .group_by(&:customer_id) .transform_values { |group| group.sum(&:total) } ``` That snippet would be five for\-loops and a hash in a less expressive language\. In Ruby it’s a paragraph that reads top\-to\-bottom\. The chain is doing the same thing a series of nested Lisp \`map\`s and \`reduce\`s would do; the syntax is dotted instead of parenthesized\. ## GetIan Johnson’s stories in your inbox Join Medium for free to get updates from this writer\. Remember me for faster sign in When Rubyists say “the language reads like English,” what they usually mean is “the collection methods compose into sentences\.” That’s Lisp’s gift, with Ruby’s punctuation\. ## Lazy enumerators Eager collection methods build the whole result, then return it\.`\[1, 2, 3\]\.map \{ \|n\| n \*2 \}`allocates a new array, fills it, hands it back\. Fine for small lists\. For large or infinite ones it’s a problem\. Lisp solved this with lazy evaluation and streams\. Scheme’s`delay`and`force`, Clojure’s lazy sequences, Haskell’s*everything*\. The idea: don’t compute the result until someone asks for it\. A list isn’t an array sitting in memory; it’s a recipe for producing one element at a time\. Ruby has the same trick\.`Enumerable\#lazy`returns an enumerator that pipes operations together without materializing the intermediate collections\. ``` (1..Float::INFINITY) .lazy .select { |n| n % 3 == 0 } .map { |n| n * n } .first(5)# => [9, 36, 81, 144, 225] ``` That pipeline reads from an infinite range\. Without`lazy`, the`select`would try to scan the whole range before passing it on; the program would never finish\. With`lazy`, each value flows through the chain one at a time, and only five of them are ever computed\. The mechanics are pure Lisp\. A lazy enumerator is a closure over the source plus a transformation\. Calling`next`advances the closure by one step\.`first\(5\)`calls`next`five times, then stops\. Everything else stays uncomputed\. You don’t reach for it often\. When you do \(paging through a large file, generating combinations until you find one that fits, walking a tree without flattening it\), there’s nothing else in Ruby that does the job as cleanly\. ## Duck typing If it walks like a duck and quacks like a duck, treat it like a duck\. Don’t check its type\. Send it the message and see what happens\. Smalltalk shares the credit here\. Smalltalk’s “send any message to any object” is closer to duck typing than Lisp’s typed\-but\-dynamic approach\. But Lisp’s tradition of dynamic typing, where values know their types and variables don’t, is part of the same lineage\. The idea that a function should care about behavior, not class, runs through both\. ``` def render(thing) thing.to_send ``` That method works for anything that responds to`to\_s`\. Strings, integers, custom objects,`nil`\. The method does not ask what`thing`*is*\. It asks what`thing`can*do*\. That posture \(behavior over identity\) is part of what makes Ruby feel forgiving\. ## Expression\-oriented design Every statement in Ruby returns a value\.`if`returns a value\.`case`returns a value\. A method returns its last expression\. A block returns its last expression\. ``` status = case response.code when 200..299 then :ok when 400..499 then :client_error when 500..599 then :server_error else :unknown end ``` That’s Lisp\. Lisp has no statements, only expressions\. Every form evaluates to something\. Ruby kept the discipline without keeping the parentheses, and the result is code that composes\. You can drop any expression into any slot\. Languages with statements ask you to write extra lines\.`if \(x\) \{ result = a; \} else \{ result = b; \}`is three lines for what should be one\. Ruby and Lisp both reject the split\.`result = if x then a else b end`\. One less variable, one less assignment to forget\. ## Code that writes code Lisp’s signature trick is that code is data\. Programs are lists, and lists are values, so a program can take a program and return a program\. Macros, Lisp’s most\-imitated and least\-replicated feature, are functions that operate on code before it runs\. Ruby doesn’t have macros\. It has the next\-best thing: a metaobject protocol that lets you reshape classes at runtime\.`define\_method`,`method\_missing`,`class\_eval`,`instance\_eval`, open classes\. None of it is as elegant as Lisp’s macros\. All of it solves the same kinds of problems\. ``` class Status %i[draft published archived].each do |state| define_method("#{state}?") do @state == state end endend ``` That generates three predicate methods at class\-definition time\. In a language without first\-class metaprogramming, you’d write the three methods by hand and accept the duplication\. The fact that you can write a loop that defines methods is a direct descendant of “code is data\.” It’s the same idea, narrower, in a language that traded macros for blocks\. This is why DSLs are easy in Ruby\. RSpec, Rails routing, Rake, Sinatra\. They look like English because Ruby’s syntax bends\. They bend because the underlying model is closer to Lisp than to C\. The closer you look at a Ruby DSL, the more you see method calls all the way down: receivers and messages like Smalltalk, with metaprogramming carving the shape like Lisp\. ## Why FP and OOP aren’t a fight It’s tempting to read all of the above as “Ruby is secretly a functional language\.” It isn’t\. Ruby is an object\-oriented language with a functional accent, and the accent is where most of the joy lives\. The functional\-versus\-object\-oriented debate is mostly a category error\. The two paradigms answer different questions\. OOP picks an abstraction \(usually a domain noun, a thing with state and behavior\) and builds from there\. FP picks a different abstraction \(a function, a transformation, a composition\) and builds from that\. The choice is which abstraction sits at the center\. Ruby picks the object\. Then it lets you call`map`on it\. You can write functional code in Ruby all day\.`users\.map\(&:email\)\.reject\(&:empty?\)\.sort\.uniq`is pure functional pipelining\. No mutation, no shared state, no surprise\. You can also write deeply object\-oriented Ruby: domain models, ActiveRecord, service objects, dependency injection\. The two styles share the file\. Sometimes they share the line\. Lisp had this conversation first\. The Common Lisp Object System is one of the most powerful OO systems ever shipped, and it sits inside a language people usually call functional\. Scheme has objects when you want them; they’re closures with a dispatch table\. The two paradigms have always been compatible\. Hostility between them is a story we tell ourselves\. What matters is the main abstraction\. Pick the one that fits the problem\. If the domain is full of behaviors\-with\-state, lead with objects and use functional methods to operate on collections of them\. If the domain is a pipeline of transformations, lead with functions and use objects to carry data through the pipeline\. Ruby supports both, because Lisp and Smalltalk both supported both, and Ruby is the language Matz built by taking the best parts of each\. ## Same shapes, different paint The expressiveness people love about Ruby isn’t original to Ruby\. It’s a careful selection from older languages, with Lisp as the largest single source\. Knowing where the ideas came from makes them easier to use deliberately, and it makes the next language easier to learn, because the ideas show up again in Clojure, in Elixir, in Scheme, in OCaml\. Same shapes, different paint\.

Similar Articles

Lisp in the Rust Type System

Hacker News Top

A Lisp interpreter embedded in Rust's trait system, allowing recursive functions, closures, and continuation passing style at compile time.

Lisp in Web-Based Applications (2001)

Hacker News Top

Paul Graham discusses the advantages of using Lisp for web-based applications, including language freedom, incremental development, and rapid bug fixing, drawing from his experience with Viaweb.

Designing Lispy DSLs, part 1: SCSS (2012)

Lobsters Hottest

This article explores the design of Lispy domain-specific languages using SCSS, a Scheme-based CSS preprocessor, as a case study. It discusses how SCSS represents CSS as first-class values and the limitations of its implementation.

Why Ruby Still Feels Like Home After All These Years

Lobsters Hottest

The author reflects on 15 years of using Ruby, praising its hidden features like refinements, delegation, and the new ZJIT JIT compiler, and notes that Ruby with ZJIT is closing the performance gap with faster languages like Go and Rust.