DEV Community

Ian Johnson
Ian Johnson

Posted on • Originally published at tacoda.Medium on

Lisp’s Influence on Ruby

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?
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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?)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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) }
Enter fullscreen mode Exit fullscreen mode

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 maps and reduces would do; the syntax is dotted instead of parenthesized.

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]
Enter fullscreen mode Exit fullscreen mode

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_s
end
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
  end
end
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)