Some Smalltalk About Ruby Loops
Posted3 months agoActive2 months ago
tech.stonecharioteer.comTechstory
calmpositive
Debate
60/100
RubySmalltalkProgramming Paradigms
Key topics
Ruby
Smalltalk
Programming Paradigms
The article discusses Ruby's loop constructs and their inspiration from Smalltalk, sparking a discussion on the merits of message passing and object-oriented programming.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
5h
Peak period
42
Day 8
Avg / period
9.5
Comment distribution57 data points
Loading chart...
Based on 57 loaded comments
Key moments
- 01Story posted
Oct 23, 2025 at 8:23 AM EDT
3 months ago
Step 01 - 02First comment
Oct 23, 2025 at 1:17 PM EDT
5h after posting
Step 02 - 03Peak activity
42 comments in Day 8
Hottest window of the conversation
Step 03 - 04Latest activity
Nov 6, 2025 at 3:59 AM EST
2 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45681023Type: storyLast synced: 11/20/2025, 2:21:16 PM
Want the full context?
Jump to the original sources
Read the primary article or dive into the live Hacker News thread when you're ready.
https://news.ycombinator.com/item?id=45644349
https://tech.stonecharioteer.com/posts/2025/ruby-blocks/
https://news.ycombinator.com/item?id=45681023
No, it's an absolutely horrible way to "add side-effects" which, usually, is already a horrible idea in its own right.
> Asking an object to iterate over itself allows objects to develop interfaces that dictate how to iterate.
That's true in pretty much any language? And since you need to know which iteration interface you need to use, it's not that much of an advantage.
> And now, when I see: `10.times { |i| puts "i = #{i}" }` I do not see a loop anymore.
Yeah, because it's not a loop: it may or may not run that block 10 times. Seriously, when a programmer's intention is "run something 10 times", the resulting expression arguably should not be "send that something to someone who, hopefully, will execute it 10 times".
Sure, you may not like that mindset, in which case, smalltalk/ruby are ABSOLUTELY not for you. You want something else.
Which is totally fine. Part of the reason behind the Cambrian-explosion of higher level programming paradigms since the 1960's is precisely because there are multiple ways to skin a cat, and different ways resonate with different folks.
I don't think Ruby's "message passing" is fundamentally different from Python's "method calls". Ultimately, both languages implementations are very similar: both look up methods by name in a hash table and then call them.
IMO "message passing" is just an alternative metaphor for what Ruby does when you type `object.name`. The metaphor fits Ruby but not Python, because the languages do three things differently:
- Ruby only looks for `name` in `object.class` (and its superclasses), whereas Python first looks in `object` itself
- If Ruby finds `name`, it's guaranteed to be a method whereas in Python it could be a different kind of value
- Ruby immediately calls the method once it's found, whereas Python (generally) doesn't - instead it returns a binding to be called later
This means that in Ruby, `object.name` is always calling a method defined on `object.class`, with `self` set to `object`. That can be re-interpreted as "sending a message" to `object.class`.
In Python, `object.name` is a more general value lookup - maybe the result will be callable, maybe not.
Except Ruby doesn't? cue `method_missing`. If you take only trivial examples you're not going to see much difference, this starts to show when you involve more advanced situations e.g with inheritance, and then you're drilling into singleton classes.
> Ruby immediately calls the method once it's found, whereas Python (generally) doesn't - instead it returns a binding to be called later
Again incorrect, `foo.bar` in Ruby and Python are two very fundamentally different things.
Python returns a binding to a method because it's an attribute accessor; when you throw inheritance into the mix it ends up that that attribute is inherited from the parent class, bound to the instance (which really in python means pass `self` as first argument), and - schematically - adding `()` after that ends up calling that bound method. If there's no attribute that's a no method error. It's all very C-ish and make believe, barely a notch above Go structs and their functions. The closest parallel in Ruby would be `foo.method(:bar).call()`
By contrast Ruby is going to send the :bar message along the inheritance chain, and if someone can respond it's going to invoke the responder's code, and surprise surprise method_missing happens only if it has exhausted inheritance but it's itself a method-slash-message; Oh and by the way the message passing is naturally so lazy that you can actually modify the inheritance chain -in flight- and inject a parent responder right before calling `super`. The whole notion of `binding` is a very concrete construct, way more rich that simply "hey I'm passing self as first argument". It becomes even more strange to "C&al. folks" when you start to involve singleton classes and start to realise weird things like Ruby classes are merely instances of the class Class and it's all instance turtles all the way down and all stupidly simple but you gotta have to wrap your head around it.
I surmise that so many differences and surprises have with Ruby are because most languages have some ALGOL legacy and Ruby is a conceptual heir to Smalltalk (and LISP†); the whole concept of open classes being another one: nothing is ever "finished" in Ruby!
Most of the time you don't have to care about these differences, until you do.
† While code isn't quite S-expr data in Ruby, there are more than enough first-class facilities that you can create and inject code entirely dynamically without resorting to `eval`ing strings.
https://lloeki.github.io/illustrated-ruby/
Each chapter kind of builds up on the previous one. It's still WIP and far from complete but what's there has helped me onboard a few people to Ruby already.
You might be interested in the Classes and Ancestry chapters.
Ruby Instances can have their own methods and override methods separate from their class.
This is the big difference, Ruby objects have no members other than methods, external entities cannot read data from the object only call methods; the object controls how it responds.
There's ways that seem to bypass this in Ruby, but they are cooperative, relying on calling other methods on the object, like accessing instance variables by calling object.instance_variable_get(:@foo).
[0] https://lists.squeakfoundation.org/pipermail/squeak-dev/1998...
It’s really hard to go back to living without this once you’re used to it.
For one, there's little ability to avoid message passing in our modern world. You can take it out of the language, but that just means pushing it to another abstraction (e.g. sockets), and all the same lack of type safety comes right back.
So imagine my delight when I found Ruby in 2005. It took the best of Perl and the best of Smalltalk and gave it a much better syntax than either, plus it had a massively growing community.
Ruby breaks a lot of the rules for what people claim they want (or should be allowed) from a programming language these days, but for me there’s still no more joyful and easy programming language to express my ideas.
Example: Swift and Kotlin can do `Int#times` and don't need message passing to get it done.
I'm the author here. While I agree that the implementation details do not matter for _most_ developers, I wanted to learn Ruby with an understand of how things are implemented.
In my previous post[1], I go into why I've always felt I didn't need Ruby since I knew Python, and I spent time learning Rust instead. It felt redundant to learn a second dynamically typed programming langauge that offered no advantage in performance. I started working at Chatwoot, where we use Ruby, and I had to pick up Ruby for the job. I didn't want to be satisfied with a "user-level" knowledge of the language and instead wanted to rip its internals apart so I can learn why it does the things it does.
Call it a preference on how I want to learn things.
[1] - https://tech.stonecharioteer.com/posts/2025/ruby/
There's a method named "ifTrue:ifFalse:" in Smalltalk (with each ":" expecting an argument, in this case, a block).
You can also chain messages and make phrases: `anObject aMethod; anotherMethod; yourself.`
The Ruby equivalent has repetition: `an_object.a_method; an_object.another_method; an_object`
or requires a block: `an_object.tap { _1.a_method; _1.another_method}` (and we usually use newlines instead of ";")
With Smalltalk, with regard to return values or chaining, you get to have your cake and eat it too.
Your methods CAN return sensible values which don't necessarily have to be the original object. BUT, if you just want to chain a bunch of sends TO A PARTICULAR OBJECT, you can use ;, and chain the sends together without requiring that each intermediate method returns the original object.
That combined with the fact that chaining sends requires no special syntax. You just write them out as a single sentence (that's what it feels like), and you can format it across multiple lines however you wish. There's no syntax requirements getting in the way.
Just finish the whole thing with a dot. Again, just like a regular sentence.
And if you find precedence or readibility becoming confusing, then just put stuff in parens to make certain portions clearer. There's absolutely no harm in doing do, even if in your particular use case the normal precedence rules would have sufficed anyway.
But yes it's a bit terse in Ruby because the default value is the last statement so it's known and predictable only if you look at the code paths. I would feel more confortable doing this if the IDE checks that it's OK.
https://pharo.org/
Somehow I hadn't thought of the two as similar or related to each other.
Anyway, certainly you can write in this style in Python, since functions are first-class objects. It just has limits on its anonymous functions, and doesn't happen to provide a `times` method on integers (at least partly because the old parser would have struggled with seeing `10.times` and thinking "this should be a floating-point number, but `t` isn't a decimal digit").
> In Python, Java or C++, calling object.method() asks the compiler to find the method in the class and call it.This is incorrect, as noted later. In Python, the lookup occurs at runtime. It also checks the object first, in most cases.
... The writing keeps anticipating my objections and then sort of correcting them and leaving something else not quite right. So trying to edit as I read has been frustrating. The point being, I just don't buy the philosophical difference that the author is trying to draw.
In Python the reason you're dealing with "attributes" rather than "messages" is because functions are first-class objects, which entails that they'll be treated the same way as ordinary data. A method call is two separate steps — the method-lookup and the actual attempt to "call" the result — which also has the implication that you can cache a "bound method" for later use. (By comparison, Ruby lets you "cache" the symbol, which in Python-think looks like some kind of magic string that tries to look itself up as an attribute name on other objects.)
But, I contend, this doesn't meaningfully change how the language is used overall.
> By including Enumerable and by implementing each, we get access to so many methods that we didn’t need to manually implement.
Okay, but in Python `map` and `filter` are just builtin functions, and you can use list comprehensions and `functools.reduce`. It's just a difference between external and internal iteration.
Finally we have the following Scheme Koan:
https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/...
Standing on the shoulders of giants, I made this little abomination: https://rikspucko.koketteriet.se/bjoli/goof-loop
It handles 98% of all loops I write, meaning I don't have to manage state (even for things like treating things like circular data). I find it removes many of the stupid errors I make when iterating over data, especially when I need nested loops and conditional accumulation.
I find it so provoking that someone implemented foreach and went "yes. That should be enough for everyone!"
It is not that it is easy to implement the iteration protocol, it is that the construct for looping universally sucks. Even the awful loop macro from common lisp runs laps around most other facilities. Racket's is even less powerful, but is miles better than what python offers.
Ruby can do some destructuring, but not enough to actually be useful.
Like the example you showcase your macro with. In Ruby it would be:
You can see what it does at a glance, you don't have to immerse yourself inside the directives to follow the logic. And when you can't do that, declaring iterators and accumulators outside a loop and then iterating and accumulating "by hand" yields code not so different than your more complex examples.I would also reach for higher order functions for most things, but when you need things to be fast that is not how you would do things. You would express it with nested for loops and ifs, and you would have to juggle state all by yourself. That is my main critique of looping constructs: they only cover the absolute simplest case, and then you are by yourself.
The partition example is not a particularly nice implementation of partition. It is there to show that you can write non-tail recursive loops, meaning you can use it to do tree walking and such. Using mutation, like your example, would make it prettier, but would make the code unsafe with schemes call/cc. Even a traditional named let (tail recursion) would be prettier.
But the point is the interface, not the implementation. Efficiency doesn't mandate assembly-like DSLs. Your interface could be as clear and clean as Ruby's and produce fast, inlined code by the power of macros. Ruby doesn't have macros, so chaining lambdas is the best it can do.
Ruby also has call/cc. None of the iterating methods has any provision to make them "safe" from it. They aren't safe from modifications to the iterated collection either. I think it makes sense; being forced to accumulate using only linked lists and having to be constantly on guard to support a rarely used quirky feature is a bad tradeoff IMO.
Continuation safety has never been a thing in ruby, so caring about it doesn't make much sense regardless of the presence of call/cc.
And lastly, neither my loops nor others I have mentioned only accumulate into lists. My loops a re in the most general sense a left fold, although right folds are possible to write as well. I provide accumulators for just about any data type available, and writing a new one yourself is pretty easy. You can express lazy loops with it as well.
All without sacrificing performance. Any performance in most cases. I haven't written much ruby since the 1.9 days, but back then I remember having to rewrite the nice .each{}.blah.blah into for loops. I could prototype in a nice way which was nice, but in the code I ended up shipping I had to juggle state using for loops because the thing I prototyped ended up being a bit slow.
I use map, filter and friends in scheme all the time, but when those are insufficient or too slow I don't have to resort to named lets. I can just use the loop macro which compiles to a named let.
Doing it that way is still going to be slower than doing it eagerly with a manual loop. I am not saying ruby should do away with the other ways of doing it. I am saying that ruby - and just about every language out there - should have a powerful looping facility that lets you do it fast without losing comfort.
If I - a non-programmer - could implement it using macros in scheme there is really no excuse for other languages to have such sucky foreach-like constructs.
For the vast majority of situations my goof-loop has no performance penalty compared to rolling the loop yourself. That is what none of the ruby examples I have been given can do. Using zip is not an acceptable way of iterating over 2 collections at once, at least not if you want to pretend to be efficient.
I don't think what I did is particularly good compared to something like the iterate macro, and all I want is for other languages to do better because I rarely have the pleasure of using scheme when doing coding.
https://ruby-doc.org/core-2.7.0/Enumerator/Lazy.html
Point is simply: No language is special or exceptional. If a language has yield, you can invert to your hearth's content. Even with lambdas/blocks you can invert by calling it in your loop. Speed is left to the efficiency of your jit or typed compilation. For example SBCL is able to compile incrementally by specifying types.
Heck, testing now on 3.4.4 a while loop is faster than using each. Summing a million numbers (to make the overhead of the different approaches as clear as possible) shows that a while loop is almost 2x faster.
This is what my complaint is about. Why is the fast path so painful to use in so many languages?
Rewriting the code you wrote (or was it the other guy? I am making food so I cant really check) to avoid intermediate collections as a for loop will be faster, especially if you can avoid creating those array pairs.
I am not saying ruby stinks. I am saying it - and just about every other language - is making it unnecessarily hard to write fast code.
https://journal.stuffwithstuff.com/2013/01/13/iteration-insi...
https://journal.stuffwithstuff.com/2013/02/24/iteration-insi...
Anyone who likes this kind of stuff, highly recommend Metaprogramming Ruby [2] by Paolo Perrota. Great look into Ruby innards and inspiring code examples. Gets me pumped
That's one of the books I'm reading now!