Hacker News new | ask | show | jobs
by drnewman 390 days ago
Blocks in Smalltalk (to my understanding) are closures. Blocks in Ruby are closures that also bring the call stack they were created in with them.

One way to think of about it is this: anonymous functions as originally implemented in early Lisps are code as an object, closures are code with its lexical environment as an object. You can think of a Ruby block as code with its lexical environment and its call stack as an object.

So they don't just handle return differently than closures, they have access to the call stack of the site where they're created like a continuation. This is why they handle return differently, but this is just one of the things that falls out from that. It also comes with other control flow features like "redo", "retry", "next", "rescue", "finally", and others. These are all call stack control (control flow) conveniences, just like return is. All of them can be thought of as being abstractions built on top of continuations (just ask a Scheme hacker).

Originally Ruby was basically a Lisp without macros, but with continuations, a Smalltalk like object system and a lot of syntactic affordances inspired by Perl, and other languages. Blocks are one of the conveniences built on top of the Lispy semantics.

Note that I'm explaining how blocks work as an abstraction (vidarh below explains how they work as a concretion, as implemented in MRI).

2 comments

> … other control flow features like "redo", "retry", "next", "rescue", "finally", and others.

At-a-glance afaict Smalltalk provides those features too, so I would guess Smalltalk blocks may have access to the call stack too?

https://drcuis.github.io/TheCuisBook/The-Debugger.html

Yes Smalltalk has continuations. So it can do all of those things as well. But I don't think they're explicitly tied to blocks like they are in Ruby. This really isn't a problem for Smalltalk since it's not as syntax oriented as Ruby.

The invovation is to have those features tied to convenient syntax.

So is the innovation to make something that was available in Lisps / Smalltalks, available within the different constraints of Ruby.

(I should check how Smalltalk blocks behave.)

I would say more broadly the innovation was two fold: 1) to make these features available in a syntactic form that would seem more familiar to programmers and 2) the powerful insight that when combined with Smalltalk style meta programming you can have a language that on the surface seems very conventional but underneath is just as powerful as Smalltalk or Lisp.

Although I would say he didn’t get 100% there although that this point Ruby isn’t too far from that.

These are ideas that I think are worth trying to take even further. In fact, I’ve been experimenting with that.

fwiw "Efficient Implementation of Smalltalk Block Returns"

https://wirfs-brock.com/allen/things/smalltalk-things/effici...

I’ll check it out. Thank you.
I don't believe this is correct. Blocks are closures.

Continuations were introduced in Ruby 1.8, via a callcc method in a Kernel module or something like that.

Since 1.9 they are in some sort of deprecated status.

I don't think that even the yield() stuff is implemented using continuations.

I agree blocks are closures. But they’re call stack aware closures. Which gives them _some_ of the power of continuations.

I’m also sure you’re right that they’re not implemented using continuations. However, my understanding is that Ruby was originally conceived as a language with continuations. I’ll see if I can find a reference for that. But from what I recall reading in a blog post from someone who was at a programming language conference in 1997 when Matz introduced the language that’s how he described it.

> they’re call stack aware closures

What does that even mean?

If you create a block deeply within a cascade of nested function calls, nineteen activation levels deep, and return that block out of all those nestings, is it still aware of the nineteen levels that have terminated, and to what purpose/benefit?

What example Ruby code would break without continuing access to the dynamic scope that has terminated, rather than just the lexical scope?

> If you create a block deeply within a cascade of nested function calls, nineteen activation levels deep, and return that block out of all those nestings, is it still aware of the nineteen levels that have terminated, and to what purpose/benefit?

A block cannot be returned, because it is not an object; it has to be used to create a Proc object with either proc or lambda semantics to be returned.

With lambda semantics, its just a closure, doesn't care about the dynamic scope, and returns immediately to whatever calls it with return/next, or returns from the calling method with break.

With proc semantics, it does retain the connection, and return or break will result in an error when those scopes have terminated, but next will still return to the caller. (You don't generally want to return a proc for that reason, the use for procs is passing down a call chain, not returning them up.)

Nice explanation
OK, so we have gone from "blocks are continuations!" to "blocks (by themselves) are a kind of downward-funarg-only thunk!" (that can be used to specify the code part of a lambda or Proc, which are not continuations either).