top | item 44054105

(no title)

maxlapdev | 9 months ago

For those unaware, Ruby blocks (and procs) are more flexible than anonymous functions as most language implement them. The article briefly goes over that, mentioning that lambda (regular anonymous functions) and procs (blocks) don't treat execute `return` the same way. There are also particularities with regard to next (continue) and break.

I made a post about the niceties of blocks: https://maxlap.dev/blog/2022/02/10/what-makes-ruby-blocks-gr...

discuss

order

chowells|9 months ago

Flexible in a way, sure. But non-locality is generally a bad property, not good. Adding it is a workaround for all the enumeration methods using blocks in a way that makes people think they're weird looping syntax instead of a fundamentally different idea.

People want to do early returns from looping over a collection, so take the easy solution of adding more messy language semantics instead of finding a semantically simple solution instead. (For that matter, have fun working out the semantics of break and next when used in a block that isn't an argument to an enumeration method. How do you as a method author opt in to distinguishing between the two after yielding to a block?)

This is generally the case with Ruby everywhere. Why does that thing have an edge case with weird semantics? To work around the edge case with weird semantics somewhere else. It's all fine if you want to just try things until something works. But if you want to really understand what's going on and write code that's a first-class participant in the language features, it gets really frustrating to try to deal with it all.

vidarh|9 months ago

It's really not that complex.

For my (buggy, unfinished, languishing without updates) prototype Ruby compiler, lambda and proc (and blocks) are all implemented nearly the same way, with the exception that for proc/blocks, return/break/next will act as if having unwound the stack to the scope where the proc was defined first (or throw an error if escaped from there).

The distinction is very obvious when you think of it in that way - a proc acts as if called in the context/scope it was defined, while a lambda acts as if called in the scope it is called in.

> How do you as a method author opt in to distinguishing between the two after yielding to a block?

You don't. A block acts as a proc, not a lambda. If you want lambda semantics, then take a lambda as an argument - don't take a block/proc and be surprised it doesn't act like something it isn't.

drnewman|9 months ago

It seems you're pretty upset about your experience with Ruby. I'm sorry that's been the case for you.

However, in Ruby blocks aren't just about flexibility, more importantly they're about generality. They're not there to resolve an edge case at all (Ruby also has keywords for loops). They're a generalization of control flow that is just slightly less general than continuations. In practical use they provide an alterative to Lisp macros for many use cases.

These were some of the problems Matz was trying to sort out with his design of Ruby--creating a language that was fun and easy to use for day-to-day programming, with as much of the meta-programming power of Lisp and Smalltalk as possible. Blocks are one of his true innovatations that came from trying to balance that tension.

dragonwriter|9 months ago

> have fun working out the semantics of break and next when used in a block that isn't an argument to an enumeration method.

The semantics are:

* next returns from the block, optionally taking a return value that will be returned as the result of the function (this is identical to the behavior of "return" from a Proc with lambda semantics)

* break returns from the function that yielded to the block, optionally taking a return value that will be returned as the result of the function (EDIT: deleted comparison to returns from a Proc with proc rather than lambda semantics here, because it wasn't quite accurate.)

This is, incidentally, also exactly the semantics when they are called from a block passed to an enumeration method, there is no special case there.

> How do you as a method author opt in to distinguishing between the two after yielding to a block?

If control returns to your method, it was a return from the block, either via return (from a block with lambda semantics), next, or running through the end of the block. If, OTOH, break is called (or return from a block with proc semantics), control won't return to the calling method normally, but code in any "ensure" sections applicable will be run, which can be used to identify that this has occurred, and even to override the return value provided by the break.

The simplest possible function illustrating this:

  def how_exited
    yield
    direct = true
    return "next, completion, or lambda return"
  rescue
    direct = true
    return "exception"
  ensure
    return "break" if not direct
  end

pmahoney|9 months ago

Can you explain further? I'm not sure I follow.

> How do you as a method author opt in to distinguishing between [break and next] after yielding to a block?

I don't use Ruby much lately, but if I yield to a block which calls break, control will pass to the code following the block (and not back to my method). If the block calls next or simply finishes, control passes back to me though I cannot know if next was called or not (but do I care? I can go ahead and yield the next element of a collection either way)

jbverschoor|9 months ago

Which is exactly what I don’t like about them. They should’ve used a different keyword or something.

This runs you into problems similar to missing a break in such statements with many language

HellzStormer|9 months ago

I don't understand what problem you are referring to? A different keyword for what? for the return? for the break?

Care to share an example problem?