Skip to content
HN On Hacker News ↗

Lisp’s Influence on Ruby

▲ 258 points 86 comments by tacoda 2mo ago HN discussion ↗

Pangram verdict · v3.3

We believe that this document is fully AI-generated

99 %

AI likelihood · overall

AI
0% human-written 100% AI-generated
SEGMENTS · HUMAN 0 of 5
SEGMENTS · AI 5 of 5
WORD COUNT 1,632
PEAK AI % 99% · §1
Analyzed
Jun 14
backend: pangram/v3.3
Segments scanned
5 windows
avg 326 words each
Distribution
0 / 100%
human / AI fraction
Verdict
AI
Pangram v3.3

Article text · 1,632 words · 5 segments analyzed

Human AI-generated
§1 AI · 99%

8 min read2 days ago--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 marksThe 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 blocksBlocks 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 # => 6The 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.

§2 AI · 99%

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 functionsOnce 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") # => falseWhat 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.

§3 AI · 99%

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 methodsmap, 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.Get Ian Johnson’s stories in your inboxJoin Medium for free to get updates from this writer.Remember me for faster sign inWhen 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 enumeratorsEager 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.

§4 AI · 99%

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 typingIf 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_sendThat 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 designEvery statement in Ruby returns a value. if returns a value. case returns a value.

§5 AI · 99%

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 endThat’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 codeLisp’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 endendThat 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.