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.
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"
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.
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”.
> 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.
With similar justification, you could argue that blocks should have a method `times` taking an integer, because repeating a block an integer number of times happens a lot in practice. I’d even argue that it is conceptually closer to blocks than to integers, and therefore the method belongs on the block. Hence you’d write `{ puts “Hello” }.times 5`.
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?
I don’t have an issue with the “everything’s an object” part, because it _is_ consistent, even though it gets a bit trippy when classes are objects as well and they are implementation of a Class class which is an implementation of itself (trickery again!).
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.
I think you’re right, but I also suspect that doesn’t clear up anything for most people as in my experience they generally don’t grok the difference unless they’ve already spent a significant amount of time in something like smalltalk or Objective-C
Perhaps I've been doing Ruby for too long, but it's still not that weird to me. The quantity "5" is very abstract without anything to have "5" of. That is why "5.days" and "5.times" exist, among others. Mathematically it makes just as much sense to start with the amount and add the unit later than it does to start with the unit and add the amount later (ie like `time_to_wait = SECONDS_IN_A_DAY * 5` as you might do in some other languages).
Maybe it is clearer if I explain it in syntactic terms? In my mental model objects are nouns (described entities) and methods are verbs - actions over the noun.
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.
It’s not just about practicality. Ruby is using message passing, not method calling. This is fundamentally different and a bit foreign to the larger community. Then ruby layers syntactic sugar on top that hides this.
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.
Although I prefer Elixir currently I agree that ruby at least goes all the way in on OO and not having to remember which feature is implemented as a language syntax and what is just a method invocation is a strength not a weakness. It is different in other languages for historical performance reasons really.
list.filter is ok! Filtering is an action that applies to a list
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.
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:
class Proc
def repeat(n)
n.times { self.call }
end
end
->{ puts("foo") }.repeat(3)
# => foo
# => foo
# => foo
Blocks are actually instances of the Proc class. There are helper methods to handle blocks passed to methods in a lightweight manner but you can also accept the block as a method argument, e.g.,
class Integer
def times(&blk)
i = 0
while i < self
blk.call(i)
i += 1
end
end
end
`5.times` is not so outlandish, though it would seem better for that to be in a `Loop` library or something (`Loop.times(5) { do some stuff }`).
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.
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.
More specifically blocks (and "proc"'s) return from the defining scope. This is just a minor clarification, but it matters, because if you pass a block down from where it is defined, and the block calls "return" it will still not just exit from the method where it was called, but from the method where it was defined.
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.
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.
But then Ruby only goes half way, not unlike the "watered-down form" in your term. Why is `#times` a method of Integer, but `#if` (or `#ifTrue`) not a method of booleans like in Smalltalk? Ruby does the same cherry picking from Smalltalk like everybody else, just different cherries.
When looking at Ruby, it feels like the simple examples are all nice and clean but then the weird details start to appear and the language feels more hacky than others (like Ned Flander's house in Simpsons S08E08).
In Smalltalk those methods don't return `true`. They take a block and evaluate it if the boolean receiving the message
(a > b) ifTrue: [ "do something" ]
EDIT: to clarify what's happening there, `>` is a message sent to `a` that will result in a boolean. The True class and False class both understand the ifTrue: message and `True>>ifTrue:` executes the block whereas `False>>ifTrue:` just throws it away.
There's no `if` keyword in the language. Control flow is done purely through polymorphism.
I apologize for my lack of Smalltalk knowledge. As you can imagine, you can do similar in Ruby by defining ifTrue to accept a block, even adding ifTrue on other all objects and defining something similar:
class TrueClass
def ifTrue(&block) = block.call
end
class FalseClass
def ifTrue(&block) = nil
end
class Object
def ifTrue(&block) = block.call
end
class NilClass
def ifTrue(&block) = nil
end
If ck45's core complaint was that this is not baked into the language, I will agree that it is less convenient for lack of a default.
problem is not with ifTrue, and not with it's performance, it's easy to do. it is "ifTrue:ifFalse:"
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.
basically it's because of "else" and "elsif". While ".each" works the same as "for .. in ...; end", it's harder to do "if else" as method which will also return value of the block inside the branch. Smalltalk can do it because "ifTrue:ifFalse:" is _one_ message, ruby didn't go that way syntactically.
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 .():
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:
That is interesting! I haven't explored Procs much, since I use ruby for a shared codebase at work and I was originally a bit afraid of trying to push unidiomatic ideas in the codebase.
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?
You shouldn't have much difficulty, Ruby converts blocks to Procs whenever it needs an actual object. Their semantics are intentionally kept the same. This is unlike lambdas, whose semantics are closer to methods.
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.
Just to clarify here: Both lambdas and procs are Proc objects. Blocks gets turned into Proc objects when you take their value.
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.
This sounds like a really innovative idea. I haven't seen a dedicated place for "collection of useful procs", but one emerging pattern is to use `app/services` and then have a bunch of single-responsibility service classes that each have call or perform method and then you use the service when you need some shared functionality. It's like a proc, but instead it's a class with `#call` method.
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.
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.
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
There are legitimate uses of method_missing (though often you'd want to call define_method dynamically instead for performance), but they tend to be when you use an object as a proxy for something (say a remote API) where you genuinely can't know the set of messages the object might need to be able to receive.
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.
Ruby is basically what happens when Smalltalk and Perl (compare the command line and the "$"-variables for example) has a baby and it inexplicably looks much better than both parents.
An interesting thing about Smalltalk and Ruby blocks is that they aren't just anonymous functions/lambdas, right? i.e. if you 'return' / '^' in a block, it's the context around the block that you return from, not just the block itself? That's what struck me about both of them when I was used to thinking in basic Lisp terms.
As much as I love Ruby (and I love it a lot - I right now have Claude fix eigenclass compilation issues in another window in my long-languishing Ruby compiler hobby project), this is also the root of a lot of confusion.
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...
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!
I'm glad folks are having fun reading this. I want to write a few more articles, particularly dissecting the internals of Ruby and how amazing it feels.
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.
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
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.
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.
I recommend reading Shopify CEO Tobi's try[0] for good example of how Ruby's block behavior and meta-programming makes it easy to create a single file, shell wrapper.
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"
They are closures. But Ruby can do interesting and slightly reckless things, like transplanting a closure into a different evaluation scope. It’s very powerful, and also very dangerous in the wrong hands.
My understanding is that the 'extra thing' is control flow - blocks can force a return in their calling scope. For example a loop that calls a block may be terminated/skipped by a break/continue statement in the block itself. However I'm not a Ruby programmer, so please check my working.
That’s just a performance optimization because blocks are typically only invoked, not passed or otherwise manipulated. If you want to do anything with the block, you pay a tiny tax and have a Proc instance.
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.
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.
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.
To an outsider watching Ruby, it's cryptic, esoteric and maybe magical. But when you actually use it and learn mechanism underneath it, things start to make sense. However, Rubys dynamic architecture have also made it difficult for DX. Things like autocomplete barely works in the ecosystem, because it's so unpredictable until runtime
Market forces will ensure this system isn’t utilized regardless of how good it is. Ruby is simply to similar to Python to consider training people solely in that.
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.
Thankfully, programming languages are subject to strong network effects but not massive ones, Ruby will continue to be a niche language that has active users and projects for many more years than you expect.
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.
These were some of the best long running sentences I’ve read in a while. A true rubyist!
> A true rubyist!
When it’s all a single paragraph, you’ll know they’ve achieved Rubynirvana.
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"
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.
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”.
> 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.
With similar justification, you could argue that blocks should have a method `times` taking an integer, because repeating a block an integer number of times happens a lot in practice. I’d even argue that it is conceptually closer to blocks than to integers, and therefore the method belongs on the block. Hence you’d write `{ puts “Hello” }.times 5`.
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?
I don’t have an issue with the “everything’s an object” part, because it _is_ consistent, even though it gets a bit trippy when classes are objects as well and they are implementation of a Class class which is an implementation of itself (trickery again!).
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.
> that “times” is a property over the “5” object
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.
I think you’re right, but I also suspect that doesn’t clear up anything for most people as in my experience they generally don’t grok the difference unless they’ve already spent a significant amount of time in something like smalltalk or Objective-C
Perhaps I've been doing Ruby for too long, but it's still not that weird to me. The quantity "5" is very abstract without anything to have "5" of. That is why "5.days" and "5.times" exist, among others. Mathematically it makes just as much sense to start with the amount and add the unit later than it does to start with the unit and add the amount later (ie like `time_to_wait = SECONDS_IN_A_DAY * 5` as you might do in some other languages).
Maybe it is clearer if I explain it in syntactic terms? In my mental model objects are nouns (described entities) and methods are verbs - actions over the noun.
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.
It’s not just about practicality. Ruby is using message passing, not method calling. This is fundamentally different and a bit foreign to the larger community. Then ruby layers syntactic sugar on top that hides this.
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.
I have been doing Ruby for so long that it feels very natural to apply a method in this way on the instance.
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.
Although I prefer Elixir currently I agree that ruby at least goes all the way in on OO and not having to remember which feature is implemented as a language syntax and what is just a method invocation is a strength not a weakness. It is different in other languages for historical performance reasons really.
list.filter is ok! Filtering is an action that applies to a list
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.
There is a bit of personal preference in what "applies to a number", but I see what you mean.
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:Blocks are actually instances of the Proc class. There are helper methods to handle blocks passed to methods in a lightweight manner but you can also accept the block as a method argument, e.g.,
When you're new it just looks like a weird-but-reasonable syntax.
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.
`5.times` is not so outlandish, though it would seem better for that to be in a `Loop` library or something (`Loop.times(5) { do some stuff }`).
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.
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.
More specifically blocks (and "proc"'s) return from the defining scope. This is just a minor clarification, but it matters, because if you pass a block down from where it is defined, and the block calls "return" it will still not just exit from the method where it was called, but from the method where it was defined.
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.
You are right on return (use next in a block), but break uses block scope.
Maybe I explained it a bit imprecise. I was trying to explain the following behavior:
This only prints "1" because the break stops the execution of the invoked method (foo).WAT? I'm a 12+ years Ruby developer and I didn't know this.
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.
But then Ruby only goes half way, not unlike the "watered-down form" in your term. Why is `#times` a method of Integer, but `#if` (or `#ifTrue`) not a method of booleans like in Smalltalk? Ruby does the same cherry picking from Smalltalk like everybody else, just different cherries. When looking at Ruby, it feels like the simple examples are all nice and clean but then the weird details start to appear and the language feels more hacky than others (like Ned Flander's house in Simpsons S08E08).
#if and #ifTrue are yours if you want them:
In Smalltalk those methods don't return `true`. They take a block and evaluate it if the boolean receiving the message
EDIT: to clarify what's happening there, `>` is a message sent to `a` that will result in a boolean. The True class and False class both understand the ifTrue: message and `True>>ifTrue:` executes the block whereas `False>>ifTrue:` just throws it away.There's no `if` keyword in the language. Control flow is done purely through polymorphism.
I apologize for my lack of Smalltalk knowledge. As you can imagine, you can do similar in Ruby by defining ifTrue to accept a block, even adding ifTrue on other all objects and defining something similar:
If ck45's core complaint was that this is not baked into the language, I will agree that it is less convenient for lack of a default.Certainly possible: add ifTrue as a method to TrueClass and FalseClass.
It just isn't very fast.
problem is not with ifTrue, and not with it's performance, it's easy to do. it is "ifTrue:ifFalse:"
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.
basically it's because of "else" and "elsif". While ".each" works the same as "for .. in ...; end", it's harder to do "if else" as method which will also return value of the block inside the branch. Smalltalk can do it because "ifTrue:ifFalse:" is _one_ message, ruby didn't go that way syntactically.
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:
as a lambda and call via #call or the shorthand syntax .(): 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:
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:That is interesting! I haven't explored Procs much, since I use ruby for a shared codebase at work and I was originally a bit afraid of trying to push unidiomatic ideas in the codebase.
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?
You shouldn't have much difficulty, Ruby converts blocks to Procs whenever it needs an actual object. Their semantics are intentionally kept the same. This is unlike lambdas, whose semantics are closer to methods.
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.
Just to clarify here: Both lambdas and procs are Proc objects. Blocks gets turned into Proc objects when you take their value.
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.
This sounds like a really innovative idea. I haven't seen a dedicated place for "collection of useful procs", but one emerging pattern is to use `app/services` and then have a bunch of single-responsibility service classes that each have call or perform method and then you use the service when you need some shared functionality. It's like a proc, but instead it's a class with `#call` method.
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)))
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.
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
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.
There are legitimate uses of method_missing (though often you'd want to call define_method dynamically instead for performance), but they tend to be when you use an object as a proxy for something (say a remote API) where you genuinely can't know the set of messages the object might need to be able to receive.
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.
I've been very taken by Ruby and how it uses blocks everywhere! This is an article I wrote just to emphasize that.
Have a look at Smalltalk blocks, or FP languages, to see where Ruby's inspiration comes from.
Ruby is basically what happens when Smalltalk and Perl (compare the command line and the "$"-variables for example) has a baby and it inexplicably looks much better than both parents.
An interesting thing about Smalltalk and Ruby blocks is that they aren't just anonymous functions/lambdas, right? i.e. if you 'return' / '^' in a block, it's the context around the block that you return from, not just the block itself? That's what struck me about both of them when I was used to thinking in basic Lisp terms.
As much as I love Ruby (and I love it a lot - I right now have Claude fix eigenclass compilation issues in another window in my long-languishing Ruby compiler hobby project), this is also the root of a lot of confusion.
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.
Yes, that is why that behaviour is known as closures.
Also why in languages like C++, you get to control what is captured from the calling context.
My next post, which is on loops, is about the common stuff with smalltalk as well!
Take a look at Kotlin, it perfected this idea
What Kotlin offers is already present in Scala or other languages from ML linage.
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!
I'm glad folks are having fun reading this. I want to write a few more articles, particularly dissecting the internals of Ruby and how amazing it feels.
Blocks yield a lot more flexibility to ruby. It was the primary reason why they are so well-appreciated.
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.
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
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.
I'm hoping the new age tooling that's coming out is going to make things better. I want to contribute to rv personally.
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.
> 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.
Yes, but it’s a core language feature, so if you spend any time programming Ruby, you’ll come to understand it.
I recommend reading Shopify CEO Tobi's try[0] for good example of how Ruby's block behavior and meta-programming makes it easy to create a single file, shell wrapper.
[0]: https://github.com/tobi/try/blob/main/try.rb
Is a block basically a lambda or is there more to it?
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"
They are closures. But Ruby can do interesting and slightly reckless things, like transplanting a closure into a different evaluation scope. It’s very powerful, and also very dangerous in the wrong hands.
Sounds a bit like a lisp macro? Or in JS using eval?
My understanding is that the 'extra thing' is control flow - blocks can force a return in their calling scope. For example a loop that calls a block may be terminated/skipped by a break/continue statement in the block itself. However I'm not a Ruby programmer, so please check my working.
I can’t unsee it either. Will try it later
Welcome
blocks are just procs :)
It does look like it but procs encapsulates a block, but a block alone is not a proc
That’s just a performance optimization because blocks are typically only invoked, not passed or otherwise manipulated. If you want to do anything with the block, you pay a tiny tax and have a Proc instance.
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.
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.
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.
With autocomplete being so good today, do we really need languages with cryptic syntax that obscure what is going on?
To an outsider watching Ruby, it's cryptic, esoteric and maybe magical. But when you actually use it and learn mechanism underneath it, things start to make sense. However, Rubys dynamic architecture have also made it difficult for DX. Things like autocomplete barely works in the ecosystem, because it's so unpredictable until runtime
Market forces will ensure this system isn’t utilized regardless of how good it is. Ruby is simply to similar to Python to consider training people solely in that.
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.
Thankfully, programming languages are subject to strong network effects but not massive ones, Ruby will continue to be a niche language that has active users and projects for many more years than you expect.
Let’s check back in 6 months after the AI bubble pops