Ruby Blocks
Posted3 months agoActive3 months ago
tech.stonecharioteer.comTechstoryHigh profile
calmmixed
Debate
70/100
RubyProgramming LanguagesSoftware Development
Key topics
Ruby
Programming Languages
Software Development
The article discusses Ruby blocks and their usage, sparking a discussion on the language's strengths and weaknesses, as well as comparisons to other languages.
Snapshot generated from the HN discussion
Discussion Activity
Very active discussionFirst comment
14h
Peak period
64
84-96h
Avg / period
16
Comment distribution144 data points
Loading chart...
Based on 144 loaded comments
Key moments
- 01Story posted
Oct 14, 2025 at 11:58 AM EDT
3 months ago
Step 01 - 02First comment
Oct 15, 2025 at 2:16 AM EDT
14h after posting
Step 02 - 03Peak activity
64 comments in 84-96h
Hottest window of the conversation
Step 03 - 04Latest activity
Oct 22, 2025 at 6:32 AM EDT
3 months ago
Step 04
Generating AI Summary...
Analyzing up to 500 comments to identify key contributors and discussion patterns
ID: 45581602Type: storyLast synced: 11/20/2025, 6:36:47 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.
Ruby has two types of closures: lambda's, where return returns to the caller, and proc's/blocks where return acts as a return in the defining scope.
Beginners often struggle with the distinction between lambda and block, and often will be wildly confused if they ever see "proc" in the wild.
Yet lambda and proc both return a Proc object, and if you take the value of a block, you also get a Proc object... Just with different return semantics...
It'd be nice if this was a bit clearer.
Also why in languages like C++, you get to control what is captured from the calling context.
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!
In terms of implementation, the closure needs to keep a reference to its environment, so it may be "transplanted" but it effectively runs in its own scope.
Should you ever do this? No. But you can. :-)
We do this regularly enough in Ruby - usually with lambda rather than proc/blocks because of the return semantics, but you can do it just as safely with proc/blocks as long as you don't call "return" within them (there's no reason to, on the other hand).
It's a way of e.g. building generator methods that returns distinct callable objects without constructing a whole class. In that case the closure environment effectively becomes the instance variables.
It’s an interesting feature to be sure, and it enables some really beautiful things, but it also enables some incredibly difficult bugs.
It usually looks pretty much the same, and so for most intents what you described is "close enough" to get most used of blocks.
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"
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.
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”.
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.
The issue is more with this part:
>The `times` method exists on the Integer classes because doing something exactly an integer number of times happens a lot in practice.
It is practical, but it breaks the conceptual model in that it is a hard sell that “times” is a property over the “5” object.
The result is cleaner syntax, I know, but there is something in these choices that still feels continually “hacky/irky” to me.
process.start() is the action of starting done by the the noun that is the process.
It's not exactly a matter of naming, as some methods are not explicitly verbs, but there is almost always an implicit action there: word.to_string() clearly has the convert/transform implication, even if ommitted for brevity.
I see no path where 5 is a noun and times the verb, nor any verb I can put there that makes it make sense. If you try to stick a verb (iterate?) it becomes clear that 5 is not the noun, the thing performing the iteration, but a complement - X iterates (5 times). Perhaps the block itself having a times object with 5 as an input would make it more standard to me (?).
But I do understand that if something is extremely practical a purist/conceptual argument doesn't go very far.
false.not
applies the not method on the false instance in the same way that
car.start
in every OO language calls the start method on car as the receiver.
So filter(list) feels just wrong when you are clearly filtering the list itself.
false.not is borderline but if read as false.negate it makes sense (negating is an action that applies to a Boolean value). That wording screws the chaining though.
5.times is where the pattern breaks: times is not an action that applies to a number (nor an action at all). It’s the block the one that should repeat/iterate - but Ruby breaks the rule there and blocks are not an object (!). If they were you could block.repeat(5) which IMO is cleaner.
No, they aren't.
Blocks can be reified into instances of the Proc class, but they are not objects and reifying them into objects has overhead.
Using a & argument asks for a block passed to the method to be reified into a proc accessible in the body of the method, which is useful if you are going to do something with it that you can't do with a bare block.
As a slight correction, a block is indeed an object! They are received by methods as an instance of the Proc class:
You can even add a 'repeat' method to these in the way that you specified, although you will need to add '->' to declare the block (as a lambda, which is also just an instance of Proc) before you call #repeat on it:After my foray into functional programming, I actually ended up appreciating Ruby more, because it lets you have it both ways: program your computer directly, and harness functional concepts. Since computer hardware is not functional I don't want the extra ceremony and abstraction over it for the sake of purity.
All that said, going back and forth between Ruby and Elixir really conceptually crystallized for me that the method call receiver is basically just the first argument to the method, accessible with the keyword `self` (which in Python is made explicit for example).
To some, 5.times seems very readable & logical. It's like arguing over the "right" colour scheme to use while coding (BTW, the correct answer is solarised light, but with black foreground text!!)
Behind the scenes everything is a message passed using __send__ and you can do this directly as well, but you generally don’t.
So when you write
5.times { puts "Hello" }
It’s sort of expected by the average programmer that you are telling 5 to call the times method and expect it to exist and do what it’s told.
In reality you have indirectly sent a message that looks like
5.__send__(:times) { puts "Hello" }
What we are really doing is sending a message to 5 (the receiver) and giving it the opportunity to decide how to respond. This is where method_missing comes in to allow responding in a custom fashion regardless if a method was explicitly defined.
So you’re not telling 5 to call the method times, rather you are asking, “Hey 5, do you know how to handle the message times?”
These are fundamentally different things. This is actually super important and honestly hard to really grok _especially_ in ruby because of the syntactic sugar. I came from a C/C++ background originally, then Java and then moved to Ruby. After a few years I thought I understood this difference, but honestly it wasn’t until I spent a couple years using Objective-C where message passing is happening much more explicitly that I was able to truly understand the difference in a way that it became intuitive.
Especially in the context of Fixnum#times. How does message passing vs method calling matter there? #times is just a method on Fixnum.
So the difference isn't just with method_missing.
With "method calling" as you put it, the program blows up if that object doesn't have that method, WHEN YOU CALL IT.
Basically, this smalltalk oo paradigm is about shifting where you put more of your logic. At & around the "call" site, or within the object whom you're calling & entrusting to do something useful with it.
All hearkening back to Alan Kay's original ideas about biology influencing how we organise code, and having a program be 1000's of "little black boxes" where work gets done by all these boxes talking to each other.
Which is why smalltalk (& ruby implements the Smalltalk object model to its entirety) actually has an awful lot in common with Erlang & other BEAM runtimes, even though those are functional languages. Because once you get beyond the techy buzzwords, the main ethos behind them is actually quite similar.
ActiveRecord has changed a lot over the years, but as an example in the original ActiveRecord you used dynamic finders. None of the finder methods existed initially, but if you passed a message to an active record object for a non existent method rather than fail it would determine if that should be a method and then it would build and persist a method to the process for future calls.
It allows for some really interesting and powerful applications in horizontally scaling as well.
The difference is fairly subtle most of the time in practice, which is why dynamic OO languages like Ruby that use message passing can mostly look like OO languages that use method calling, though it is significant behind the scenes and opens up a lot of possibilities; one of the more obvious practical differences is that it is why Ruby can capture and handle unknown "method calls" via "method_missing", which works the way it does because the version of __send__ defined in BasicObject looks for an existing method matching its first argument in the object's inheritance chain, and if it finds that calls it, and if it doesn't find one does the same thing with the "method_missing" method.
There's a lot that can be built on top of this, either leveraging method_missing or more directly.
Though I think the more relevant issue upthread isn't message passing vs method calling but narrow vs broad conception of nouns/agents.
Ruby is almost a pervasive OO system (falling short because blocks are not objects, though they can be reified into objects with some efficiency overhead.) As such, unreified blocks are the only thing in Ruby that cannot be the agent of action.
For an iteration of a block a fixed number of times, the potential agents are the block and the number of times. But blocks aren't objects, so they can't be the agent, leaving only the number.
Hence, Integer#times
Maybe here is the confusion, ruby is based on message passing, so the `times` is a message you are sending to 5, not a property of it.
When you learn the language you really fall into two camps:
- ah, yes, this is unusual, but it's consistent and now i understand the language
- this is way too clever
I'm more in the first camp.
The `5.days` example that was posted somewhere else in this thread might be a better example. It is not, as far as I can tell, part of Ruby's core library, thank goodness, but it is the kind of thing the methods-as-hacks-for-syntax culture seems to encourage. My question being "why the heck should the number 5 know anything about days? What does 5.days even mean, given that there are various ways to interpret 'a day'?"
This kind of bad design has made its way all over the place. The Java mocking libraries that my coworkers like to use are full of it. Long chains of method calls that appear like they're trying to 'look like English' but make no damn sense, so you have to dig into each method call and what kind of thing it returns to understand what this chain actually means.
I beg to differ. What object does the method `puts` belong to? Why do you not call puts with its objects name?
Ruby has a concept of mixins (Golang interfaces), these are not objects. Neither is `puts`
puts is a method which has a class: the Method class:
Everything being a confused muddle in Ruby, there is evidently some Kernel base class that is injected into every Object, and puts is a private method in that: The Method class of puts is a real class with methods and all:It really isn’t a confused muddle, the rules are very clear. Just because it doesn’t match what you expect from your other language experience doesn’t mean it isn’t clear.
It's also the opposite of magic; magic is when language features can't be described in terms of the language itself.
The Kernel module is included in the Object class, which means its methods are available to every Ruby object and can be accessed from virtually any scope, including the top-level (global) context where self is an instance of Object called "main."
Mixins are just modules, which are objects, which you can call methods on. (Or rather, send messages to) You can easily verify this in irb calling a method on (for example) the Enumerable module:
You are right that a module is not a class, and it is not possible to call `.new` on it. But the module itself is very much an object.In the first place, I'd say what you're asking for goes beyond "everything is an object".
But I think your questions can be answered in a way that affirms that "everything is an object" in Ruby anyway.
> Why do you not call puts with its objects name?
Because it belongs to whatever object you're working in already; `puts` is identical to `self.puts`. And yes, you're always working in an object: https://bparanj.gitbooks.io/ruby-basics/content/chapter1.htm...
> What object does the method `puts` belong to?
As indicated above, it belongs to the object `self`. It gets added object via the mixing-in Kernel module into Object. Kernel is itself an instance of class Module: https://docs.ruby-lang.org/en/3.4/Module.html
The `puts` in Kernel delegates to `puts` from IO, which is likewise an instance method belonging to the object you can refer to by the name `$stdout`: https://docs.ruby-lang.org/en/3.4/IO.html
> Ruby has a concept of mixins [and], these are not objects.
Sure they are. Mixins themselves inherit from Module, and Modules are also objects (just like classes are).
Some highlights from Chapter 27 ("Library Reference: The Class Model") of the recent edition of the pickaxe book (emphasis mine):
> The Kernel module is included by class Object, so its methods are available in every Ruby object. One of the reasons for the Kernel module is to allow methods like `puts` and `gets` to be available everywhere and even to look like global commands. Kernel methods allow Ruby to still maintain an "everything is an object semantics".
and regarding mixins:
> The Module class is the class of any module you declare with the `module` keyword. Each module is an instance of class Module.
on Object:
> Object is the parent class of (almost) all classes in Ruby unless a class explicitly inherits from. BasicObject. [...] Object mixes in the Kernel module, making the built-in functions globally accessible.
tl;dr: mixins in Ruby are instances of class Module, and their methods end up bound to instances of class Object. Abstract module methods that don't belong to a concrete instance of some class that mixed in their module belong to the Object that is the module itself (the instance of class Module). (The same kind of thing is how class methods work.)
Ruby "mixins" are the affordance of sharing instance methods from a module via a keyword. Modules are objects, and are instances of class Module:
>Neither is `puts`Like all methods, `puts` is an object:
Here you see evidence of where `puts` comes from: Kernel#puts via Object, which I will now explain in detail.>What object does the method `puts` belong to?
It belongs to the object you are calling it from within. You don't need to call `puts` with a receiver because it is an instance method, just like you don't need to call an instance method `foo` via `self.foo`. But you could choose to use a receiver, since the `puts` you know and love is just another instance method. You can try `self.puts` for yourself in some context!
Your classes (and their instances) inherit this `self.puts` instance method from the Object class, which includes the Kernel module, which provides `Kernel#puts`. So the only reason you can send it as a message without a receiver is because it is just another instance method (again, the same as calling instance method `#foo` without using `self.foo`).
Caveat: You can build an "alternate universe" object hierarchy by inheriting from BasicObject, and in your alternate universe, you can choose to not `include Kernel`, and you will see that instances of your new objects do not have access to `puts` in their instance scope.
But blocks are not objects in Ruby, so you can’t do that, and everything actually isn’t an object in Ruby.
Also, it’s even more common to do something depending on whether a condition is true or false, but true and false in Ruby don’t have a method to (not) execute a block on them, and you use a non-OOP `if` instead, so what’s up with that?
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.
This can sometimes be useful: A calling method can pass down a block or proc to control if/when it wants an early return.
Basically Ruby has two types of closures:
* A return in a lambda returns to the calling scope. So basically, it returns to after where the "call" method is invoked.
* A return in a block or a proc returns from the scope in which it was defined (this is also why you get LocalJumpError if you try to return a block or a proc, but not a lambda to the method calling the one where the block or proc is defined).
When you name a block, you get a Proc object, same as you get when you take the value of a lambda or proc.
In practice, that blocks in MRI are not Proc objects already is just an implementation detail/optimisation. I have a long-standing hobby project to write a Ruby compiler, and there a "proc" and a bare block are implemented identically in the backend.
There's no `if` keyword in the language. Control flow is done purely through polymorphism.
It just isn't very fast.
also it is common to do assignments in the "if", and with actual method and blocks scope of the introduced variable would be different and everyone would be tripping on it all the time.
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:
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): 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: And doing similar, but with a lambda:In your experience, is it ok to use Procs for example for extraction of block methods for cleanliness in refactors? or would I hit any major roadblocks if I treated them too much like first-class functions?
Also, is there any particular Rails convention to place collections of useful procs? Or does that go a bit against the general model?
Pass the wrong number of of arguments to a Proc or block, it will pass nil for missing args and omit extras. Pass the wrong number of arguments for a method or lambda and you get an ArgumentError. Use the return keyword in a lambda, it returns from the lambda, just like if you call return in a method. In a block or Proc, it returns from the calling method.
So I would feel comfortable leaning on them for refactoring as it's as Ruby intended. Just use lambdas when you want to turn methods into objects and Procs when you want to objectify blocks.
You should get ahold of a copy of Metaprogramming Ruby 2 if you find yourself refactoring a lot of Ruby. It's out of print, but ebooks are available.
So just be clear about whether you're talking about a proc or a Proc...
> In a block or Proc, it returns from the calling method
No, in block or a Proc it returns from the scope where it was defined.
Usually this is the same, and so it doesn't usually matter much, but if you pass a proc or a block down to another method, then a return within the block will still return from the method where it was defined.
This can occasionally be useful, as you can use it to pass in a predicate to a method that can control if/when you want to return, irrespective of how deeply nested.
> No, in block or a Proc it returns from the scope where it was defined.
Should of course read:
> No, in a block or a proc it returns from the scope where it was defined.
In a block or proc, it returns from the method in which the block/proc was defined, not the calling method. These may or may not be the same; the proc may be called deeper in the call chain, or it might (erroneously) be called after the defining method had exited, producing a LocalJumpError.
This also means that implicit returns from a block/proc have different semantics than explicit returns, unlike lambdas/methods where implicit returns have the same semantics as explicit returns. (From a block/proc, the last evaluated expressions behaves as if it were passed to "next" rather than "return", resulting in a local return to the calling method rather than a return from the defining method.)
It's called the "callable" pattern.
>is it ok to use Procs for example for extraction of block methods for cleanliness in refactors?
As others have said, use Lambdas via `->() { do_thing }` over Procs if you want to pass them around. They are the "true" way to use first-class functions.
Also, if you want to pass methods around, you can do so, just be aware that they carry everything you might expect with them: they are called with the receiver still being the object they came from. Callbacks are uncommon in Ruby, but I've seen a few instances of passing methods of the current object into some other as a callback, e.g.
Then #do_complex_thing can use `on_success.call` and `on_failure.call`, which is nice when you want to stub these in unit tests without resorting to stubbing/mocking. You can just pass lambdas (first-class functions) as the value for :on_success and :on_failure kwargs in tests.>Also, is there any particular Rails convention to place collections of useful procs?
No convention for a "place" for these. I generally dislike how Rails got people into using directory names for the "kind" of a module/class instead of actually mirroring the architecture. This is kind of why.
If what you really desire is a big bag of pure functions in the global scope, you should still at least put them inside of a namespace (a Ruby module). For example, if you wanted a big bag of type coercions, you might put them into a module:
However, this isn't necessarily very efficient. Each call to CommonCoercions.currency is going to instantiate a new Lambda instance! You could alleviate this by caching the value in a singleton-scope instance variable: Even then, this isn't really common in Ruby. You might as well just have these be singleton methods outright (aka "class methods"): If you really want to pass them around as first-class functions, you can grab a reference to that function like so: And then use them via `.call()` the same as any other lambda if you want to pass them around.However, if you want to use them only locally within a particular class, in a Rails project you are better off just calling them directly on the module, or even delegating so that the method is available in the class as though it were a local instance method:
In addition to the efficiency issues of unnecessarily reifying blocks into Proc objects, there are some sharp edges that can cut you doing this. For instance, Ruby blocks and procs [0] have return semantics based on where they are defined (specifically, "return" in a block or proc returns from the method in which the block/proc is defined.) So if you have two identical blocks used in two different methods, and those blocks have return calls, and you refactor them to shared Proc defined somewhere else that is used in each method, you will find that the behavior is broken, and when it returns it will either return from the wrong method or produce a LocalJumpError, depending on the exact structure of the program.
Local return from a block/proc is done with "next" instead of "return" (and if neither a next or return is hit, the last expression evaluated will be treated as an implicit "next"), so a block without an explicit return can be converted to a proc and reused in different places more safely.
[0] using the convention of lowercase "proc" to refer to a Proc object that does not have lambda semantics, whereas those created with Kernel#lambdda or the stabby-lambda syntax are "lambdas".
E.g. 3:
(f, x) => f(f(f(x)))
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.
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.
If someone plays silly buggers and invokes them under instance_eval or class_exec etc that fiddle with self or definition contexts then some of this goes out the window, but those are special-purpose methods that come papered with red flags. This is typically seen in poorly designed DSLs that are trying too hard to pretend they’re not actually Ruby. If memory serves, the Chef DSL was a prime example in this regard. If the language was stricter, then sure, this wouldn’t be possible. But debugging these cases isn’t super hard either once you know the limited range of culprits, and the fix is always the same: place values in local stack variables to rely on them in a closure.
Using blocks for callbacks is fine. Don’t make assumptions about the semantics of flow control statements that other languages may have imposed on you, i.e. use next and break for explicit block results and local exit instead of return, and don’t eval them.
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.
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.
Not super relevant, but my favorite fun fact about Python (specifically CPython) is that hash(x) where x is an int is always the value of the int - except for -1, because the underlying C function uses -1 to represent errors. So hash(-1) == -2, which means you have a hash collision right around 0 where hash(-1) == hash(-2)
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.
Yes, but it’s a core language feature, so if you spend any time programming Ruby, you’ll come to understand it.
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
Method missing is a different beast altogether. I would probably avoid it nowadays.
[0]: https://github.com/tobi/try/blob/main/try.rb
It’s a hard reality because I’m sure Ruby is better according to some criteria, but be realistic their share of the market is going to shrink until it’s not really an option of most large companies.
I know people will disagree with me, but I wish I knew earlier in my Carr how little this sort of thing matters.
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"
1. Someone scoffs at you for writing ruby instead of python / go / javascript / kotlin
2. Then they read the code
3. Then they install ruby
It's an amazing language and remains my go-to for anything that doesn't need protocol stuff.
As for lack of tooling, it apparently wasn't a priority when the language was designed. I'm guessing the emphasis was more on the ease of reading/writing, and having internal logical consistency. How such language design decisions make the development of tooling more difficult was a secondary concern.
I have an "on save" hook that runs ripper-tags on every file save. This keeps the definitions always up to date.
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
There is: https://sorbet.org/
Also Ruby has RBS now which is not inline and... much maligned to say the least. I think the entire ecosystem is at a crossroads rn wrt typed Ruby
Here's with Minitest (part of std)
If you want even closer to yours, the following works just fine Better?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.
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.
1 more comments available on Hacker News