top | item 45581602

Ruby Blocks

193 points| stonecharioteer | 5 months ago |tech.stonecharioteer.com | reply

145 comments

order
[+] inopinatus|5 months ago|reply
For this audience it may be worth noting that Ruby’s blocks are closures and are passed to methods either anonymously/implicitly or as a named parameter, may be subsequently passed around to any collaborator object, or otherwise deferred/ignored, have the same range of argument arity as methods and lambdas, can even be formed from (and treated similarly to) lambdas, and are thereby fundamental to Ruby’s claim to being a multiparadigm language even as they also betray the Smalltalk roots.

In addition they have nonlocal return semantics, somewhat like a simple continuation, making them ideal for inline iteration and folding, which is how most new Rubyists first encounter them, but also occasionally a source of surprise and confusion, most notably if one mistakenly conflates return with result. Ruby does separately have callcc for more precise control over stack unwinding, although it’s a little known feature.

[+] rubyfan|5 months ago|reply
These were some of the best long running sentences I’ve read in a while. A true rubyist!
[+] dragonwriter|5 months ago|reply
> can even be formed from (and treated similarly to) lambdas

They are also used to create lambdas (even the shorthand stabby-lambda syntax desugars to a call to Kernel#lambda with a block.)

> Ruby does separately have callcc for more precise control over stack unwinding, although it’s a little known feature.

callcc is included in CRuby but has been sidelined from Ruby as a language separate from CRuby as an implementation for a while, with Fibers understood to cover the most important use cases for callcc.

[+] OptionOfT|5 months ago|reply
As someone who comes from strict languages (the more strict, the better) Ruby blocks are... not fun.

I've seen them used in situations where they are used like a callback, but due to the nature of how you write them, you have no clue whether the variable you're referring to is a local or a global one.

This makes debugging incredibly hard.

[+] Dan42|5 months ago|reply
This is really cute and heartwarming.

Back in the day, a lot of people including me reported feeling more comfortable in Ruby after one week than all their other languages with years of experience, as if Ruby just fits your mind like a glove naturally.

I'm glad new people are still having that "Ruby moment"

[+] nixpulvis|5 months ago|reply
Ruby is still the first tool I reach for if I need to do something quickly and naturally. People are always surprised at how readable the code comes out even compared to python.
[+] stonecharioteer|5 months ago|reply
Thank you for reading this. I'm having fun learning Ruby. I just started working at a company where I use it full time. It's great learning it and I have supportive colleagues who are excited for me. I'm going to write more about Ruby. I have planned about 6 articles in the next few weeks. I hope I get around to them all.
[+] kace91|5 months ago|reply
Coming from a language with functions as first class objects, blocks felt a bit limited to me, because it feels as if you almost have functions but not really, and they get inputted by a back door. Used for example to:

let isLarge = a => a>100;

numbers.filter(isLarge)

Blocks let you do the same but without extracting the body as cleanly. Maybe it’s a chronological issue, where Ruby was born at a time when the above wasn’t commonplace?

>When you write 5.times { puts “Hello” }, you don’t think “I’m calling the times method and passing it a block.” You think “I’m doing something 5 times.”

I’m of two minds about this.

On the one hand, I do agree that aesthetically Ruby looks very clean and pleasing. On the other, I always feel like the mental model I have about a language is usually “dirtied” to improve syntax.

The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used.

These magical tricks are everywhere in the language with missing_method and the like, and I guess there’s a divide between programmers’ minds when some go “oh that’s nice” and don’t care how the magic is done, and others are naturally irked by the “clever twists”.

[+] WJW|5 months ago|reply
> The value 5 having a method, and that method being an iterator for its value, is kinda weird in any design sense and doesn’t seem to fix any architectural order you might expect, it’s just there because the “hack” results in pretty text when used.

I don't think this is particularly weird, in Ruby at least. The language follows object orientation to its natural conclusion, which is that everything is an object, always. There is no such thing as "just data" in Ruby, because everything is an object. Even things that would just be an `int` in most other languages are actually objects, and so they have methods. The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice.

[+] judofyr|5 months ago|reply
Blocks are fundamentally different from functions due to the control flow: `return` inside a block will return the outer method, not the block. `break` stops the whole method that was invoked.

This adds some complexity in the language, but it means that it’s far more expressive. In Ruby you can with nothing but Array#each write idiomatic code which reads very similar to other traditional languages with loops and statements.

[+] jhbadger|5 months ago|reply
If you are familiar with a true object-oriented language like Smalltalk (rather than the watered-down form of OO in C++, Java, etc.), an integer like 5 having methods makes sense because it (like everything else) is an object. Objects in Ruby aren't just window dressing -- they are its core.
[+] vidarh|5 months ago|reply
This is your example in Ruby:

    isLarge = -> {|a| a > 100 }

    numbers.filter(&isLarge)
Or you could replace the first line:

    isLarge = -> { _1 > 100 }
Some people hate that syntax, though. I think for trivial predicates like this, it's fine.
[+] somewhereoutth|5 months ago|reply
Interestingly, in the Lambda Calculus, where everything is a function, a standard representation for a natural number n (i.e. a whole number >= 0), is indeed a function that 'iterates' (strictly, folds/recurses) n times.

E.g. 3:

(f, x) => f(f(f(x)))

[+] chao-|5 months ago|reply
The "aesthetically pleasing" aspect of blocks is not mutually exclusive with real, first-class functions! Ruby is really more functional than that. Ruby has both lambas and method objects (pulled from instances). For example, you can write:

  let isLarge = a => a>100;
as a lambda and call via #call or the shorthand syntax .():

  is_large = ->(a) { a > 100 }
  is_large.call(1000)
  # => true
  is_large.(1000)
  # => true
I find the .() syntax a bit odd, so I prefer #call, but that's a personal choice. Either way, it mixes-and-matches nicely with any class that has a #call method, and so it allows nice polymorphic mixtures of lambdas and of objects/instances that have a method named 'call'. Also very useful for injecting behavior (and mocking behavior in tests).

Additionally, you can even take a reference to a method off of an object, and pass them around as though they are a callable lambda/block:

  class Foo
    def bar = 'baz'
  end

  foo_instance = Foo.new
  callable_bar = foo_instance.method(:bar)
  callable_bar.call
  # => 'baz'
This ability to pull a method off is useful because any method which receives block can also take a "method object" and be passed to any block-receiving method via the "block operator" of '&' (example here is passing an object's method to Array#map as a block):

  class UpcaseCertainLetters
    def initialize(letters_to_upcase)
      @letters_to_upcase = letters_to_upcase
    end

    def format(str)
      str.chars.map do |char| 
        @letters_to_upcase.include?(char) ? char.upcase : char
      end.join
    end
  end

  upcase_vowels = UpcaseCertainLetters.new("aeiuo").method(:format)
  ['foo', 'bar', 'baz'].map(&upcase_vowels)
  # => ['fOO', 'bAr', 'bAz']
This '&' operator is the same as the one that lets you call instance methods by converting a symbol of a method name into a block for an instance method on an object:

  (0..10).map(&:even?)
  # => [true, false, true, false, true, false, true, false, true, false, true]
And doing similar, but with a lambda:

  is_div_five = ->(num) { num % 5 == 0 }
  (0..10).map(&is_div_five)
  # => [true, false, false, false, false, true, false, false, false, false, true]
[+] user3113|5 months ago|reply
Ruby is my favorite language. There is no other language with same ergonomics. I dont understand hype around ts and all frameworks that changes every week.
[+] stonecharioteer|5 months ago|reply
I've been very taken by Ruby and how it uses blocks everywhere! This is an article I wrote just to emphasize that.
[+] janfoeh|5 months ago|reply
I discovered Ruby (through Rails) about twenty years ago on the dot. Coming from Perl and PHP it took me a while, but I remember the moment when I had the same realisation you did.

I still love this language to bits, and it was fun to relive that moment vicariously through someone elses eyes. Thanks for writing it up!

[+] pjmlp|5 months ago|reply
Have a look at Smalltalk blocks, or FP languages, to see where Ruby's inspiration comes from.
[+] PufPufPuf|5 months ago|reply
Take a look at Kotlin, it perfected this idea
[+] Alifatisk|5 months ago|reply
I believe the underlying behaviour of Ruby blocks is one of those mechanics that isn't talked about that much for newcomers, they just get used to how Ruby code look like when they see Rails, Cucumber or RSpec

Blocks and method_missing is one of those things in Ruby is what makes it so powerful! I remember watching a talk where someone was able to run JS snippets on pure Ruby just by recreating the syntax. That proves how powerful Ruby is for creating your own DSL

It's also a double edged sword and something you have to be careful with on collaborative codebases. Always prefer simplicity and readability over exotic and neat tricks, but I understand the difficulty when you have access to such a powerful tool

[+] nasmorn|5 months ago|reply
IMO blocks are not something to be careful about in ruby. If you don’t use blocks you are the weirdo in this language.

Method missing is a different beast altogether. I would probably avoid it nowadays.

[+] shevy-java|5 months ago|reply
Blocks yield a lot more flexibility to ruby. It was the primary reason why they are so well-appreciated.
[+] rockyj|5 months ago|reply
Ruby is still so good to read and hack things with. It is a shame that it is not so popular and you know the reasons why. I still wish with such a good DSL friendly structure, it should have become an IaC de-facto standard language or used in some niche where I could use it freely without question.
[+] culi|5 months ago|reply
The biggest thing holding Ruby back is lack of gradual typing imo. I honestly think javascript is a better fit for IaC. Not only is already the language of the web (everyone has to know some javsacript) but JSDOC is supported by most IDEs giving it gradual typing. Many people don't realize jsdoc is typescript and is a full replacement.

Nowadays I like to reach for Julia for quick one-off scripts and webscraping. It has a beautiful and accessible syntax with built-in gradual typing. I would love to see it more widely adopted in the IaC world

[+] rapind|5 months ago|reply
It was revolutionary for readability and has influenced a lot of newer languages. That's a pretty good legacy IMO.
[+] t-writescode|5 months ago|reply
Ruby Blocks and how they're used literally everywhere is one of the hallmarks of why it's so nice to write the first time in that language; and it shaped how I use and select future languages.

Ruby Blocks are almost certainly the reason why I love Kotlin so much - it feels like a well-typed, curly-bracket-styled Ruby in those ways. The collection operation chains in both languages just. feel. good. And I blame Ruby for my first exposure to them, and possibly a lot of people's early exposure to them that helped languages that came after become better.

Ruby does take it to a-whole-nother level though, in particular with its 'space as separator' syntax, so you can make a *robust* DSL that's even more powerful than Kotlin's "if the last param is a function, you can just put a curly bracket and go" style.

[+] xigoi|5 months ago|reply
I still don’t get how

  it "adds two numbers" do
    calc = Calculator.new
    expect(calc.add(2, 3)).to eq(5)
  end
is supposed to be more readable than

  test "add two numbers":
    let calc = Calculator.new
    check calc.add(2, 3) == 5
(The latter is Nim with the std/unittest module.)
[+] Alifatisk|5 months ago|reply
Your example is Rspec, a testing framework for Ruby, not everyone enjoys it

Here's with Minitest (part of std)

    it "adds two numbers" do
      calc = Calculator.new
      assert_equal 5, calc.add(2, 3)
    end
If you want even closer to yours, the following works just fine

    assert calc.add(2, 3) == 5
Better?
[+] thepaulmcbride|5 months ago|reply
Ruby is really let down by the tooling around the language. The language itself would be so much more fun to write if the lsp would reliably jump to the definition of functions etc that seem to appear out of no where. It has been the biggest source of frustration for me while learning Ruby.
[+] Mystery-Machine|5 months ago|reply
You're right! Although I get faily far by using Bust-a-gem VS Code extension. (The underlying ripper-tags gem can work with any IDE) https://github.com/gurgeous/bust-a-gem

I have an "on save" hook that runs ripper-tags on every file save. This keeps the definitions always up to date.

[+] stonecharioteer|5 months ago|reply
I'm hoping the new age tooling that's coming out is going to make things better. I want to contribute to rv personally.
[+] mmcromp|5 months ago|reply
I say the opposite, the lack of tooling highlights the weakness of the language. The drive to make it declarative/mimic "natural" language by reshuffling and overloading can be "delightful" to some, but beyond the paper covered surface is a mess. And for what ? "5.times do something unless" isn't cute to me. It's a dog "talking" by putting peanut butter in his mouth. But I think I'm the only one who feels this way
[+] Bolwin|5 months ago|reply
And it's twice as bad on windows.
[+] teaearlgraycold|5 months ago|reply
This level of cuteness and obsession with syntax is partly what drives me away from Ruby. A function should just be a function. We don't need to make programming languages look more like English. There are certainly issues with other languages and their verbosity (like with Java). But I don't want to have more ways to do the same thing and worry about how poetic my code reads as English - we should worry how poetic it reads as computer code.

That said, we don't need just one programming language. Perhaps Ruby is easier to learn for those new to programming and we should introduce it to students.

[+] stonecharioteer|5 months ago|reply
I think Ruby teaches a sense of style, but I'm not sure that style carries over to other languages. Python was my primary language for 12 years but I'm disappointed in the depth of Knowledge python Devs have. Most barely understand the language or try to. Ruby seems to coax people into coding into a ruby way. I like that.
[+] hshdhdhehd|5 months ago|reply
Is a block basically a lambda or is there more to it?
[+] masklinn|5 months ago|reply
Assuming that by lambda you mean "an anonymous function"[1], for most intents and purposes they are, except their returns are non-local whereas functions usually have local returns.

However blocks are special forms of the language, unless reified to procs they can only be passed as parameter (not returned), and a method can only take one block. They also have some oddities in how they interact with parameters (unless reified to lambda procs).

[1] because Ruby has something called "lambda procs"

[+] vidarh|5 months ago|reply
Basically. But Ruby has two types of lambdas, one called lambda, and one called proc. Blocks act like the second type.

The difference is that if return is called in the former, you return to the expression after where it is called, while I'm the latter return returns from the scope where it is defined.

This is usually going to feel reasonably intuitive, but can become weird if you e.g. reify a block into a named object and pass it around.

It can be very useful though. E.g. you can pass a block down into a recursive algorithm as a means to do a conditional early exit when a given criteria is met.

Notably you can do this even if a piece of code takes a named argument instead of a block and doesn't anticipate that use and expect you to pass a lambda instead of a process...

In most of the typical uses of a block, most people will just naturally expect the behaviour you see, because the block looks and feels like part of the scope it will return from.

[+] hambes|5 months ago|reply
This is the first time I ever got ruby. Reading it always felt like magic to me and I think it's still too much, if I really want to understand the abstractions, which unfortunately I do. But the understanding of blocks and especially instance_eval has helped a lot.
[+] stonecharioteer|5 months ago|reply
I'm glad I helped a bit. I'm going to write a few more posts since I just started a job where I need to do Ruby full time. I'm writing about Loops today.
[+] bashkiddie|5 months ago|reply
I heard some rumor that ruby was a type 2 lisp. There were some guys who rate programming languages on how much lisp features they reassemble. What is the source?
[+] politelemon|5 months ago|reply
It's only more readable if you already understand it. Otherwise it is not, it requires the same kind of hand waving that happened at the start.

Important to understand that readability doesn't mean it should be closer to natural language, in programming it means that a junior dev troubleshooting that code later down the line can easily understand what's happening.

The python examples are certainly more readable from a maintainability and comprehension standpoint. Verbosity is not a bad thing at all.

[+] Mystery-Machine|5 months ago|reply
Ruby is beautiful.

It's weird, and different and therefore a bit repulsive (at least to me it was) at first. But, once you learn it, it's so easy to read it and to understand what's going on.*

* Side-note: Sometimes variables or methods look the same as parenthesis () are optional. So, yes, there's more things that can look like magic or be interpreted in multiple ways, but more times than not, it helps to understand the code faster, because `clients` and `clients()` (variable or method) doesn't matter if all it does is "get clients" and you just need to assume what's stored/returned from that expression. Also "get clients" can be easily memoized in the method implementation so it gets as close as possible to being an actual variable.

[+] rvitorper|5 months ago|reply
I can’t unsee it either. Will try it later