177 comments

[ 5.4 ms ] story [ 267 ms ] thread
Love this series
It makes me want to go back and apply these new tricks to old code, splendid.
Yeah, almost made me think of unquitting my old job for half a second or so.
(comment deleted)
This is sweet like candy. Such a fun series!
(comment deleted)
> please note that `...` in the code below is the exact valid syntax, not “code omitted for a blog post”

We need a standard for this. When I'm writing example code, or redacting real code, I usually opt for an ellipses with spaces: `. . .`

`...` is the standard.

Don't make language features that use three-dot-ellipsis as a keyword. Just like don't make a language where "Hello world" is a special string with special significance, or where variables named foo, bar, baz, quux, or i have special meaning.

I mean, it's nobody's fault but your own if you make a language feature that breaks industry conventions.

Both Java and JavaScript use the three-dot-ellipsis `...` (spread, varargs), so I'm not sure if it 'breaks industry standards' if Ruby uses them as well.
R and its predecessor from 1988, S-Plus, as well.
It's a lot less confusing when it's only appearing in the function's parameter signature where you're declaring parameter names instead of the method body. It's not common to see educational snippets with ellipsis in the function signatures - they're usually in the body somewhere.
Three-dots is valuable real estate. I don't begrudge the various languages that use it - their use cases are usually worth it.
C has used ellipses for variable arguments ca 1985.
Can always use comments /* code */
The point of not using comments in those cases is to make it clear that this is not intended to be valid code.
I think that was the point. If it wasn't clear enough that your ellipses are meant to be showing some thing that was elided, put them in a comment.

(In this case, the article used them as valid syntax and added a comment to clarify that this is indeed valid syntax!)

If that isn't what you were doing, and it still isn't clear enough, add some text to the comment about what the "intentionally omitted part" was that you covered by an ellipsis.

I think the root comment was about having a standard for denoting that some code is omitted. In that case, one doesn’t want the code sample to be syntactically valid, in order to unambiguously communicate that it is incomplete. Using comments, however, would typically make the code syntactically valid.
Some languages (Rust and Swift for example) have expressions which are placeholders but won't actually work.

  let x = 5 + todo!("Complicated arithmetic expression");
... is valid Rust, it typechecks, your syntax highlighter should be fine with it, it'll compile, but if that line ever executes, you panic immediately.
Personally I think execution time is too late.
I disagree. In Rust it's extremely useful to be able to tell if your types are correct and the code will compile with the signatures given, and with multiple placeholders, you can iteratively replace them and test them progressively.

I use them heavily during development. Most of my new functions in progress end with an unimplemented!

I'm not sure what advantage at all could be gained by preventing it from compiling, other than to save people who copy code without even looking at it some minor inconvenience.

Edit: I suppose if you really wanted an existing macro to cause compilation to fail, you could use https://doc.rust-lang.org/std/macro.compile_error.html

But I think that's a strange use of it.

I’m sure we can come up with a language where three dots separated by spaces is valid syntax.
Befunge
Brainfuck, as well. And technically Whitespace though that's maybe not as useful.
(comment deleted)
A non-esoteric answer: in Forth dot prints the top of the stack and function calls are separated by spaces. Hence three dots separated by spaces is the simple and straightforward way to print the top three stack elements.
It's valid in PowerShell, if "." is defined as a function:

  PS> function . {$Args}
  PS> . . .
  .
The first dot is an operator which invokes a command in the current scope, the second dot is our command ".", and the third dot is passed to the command as a string.
You can also do this in Bash.

  $ .(){ "$@"; } # define function called "."
  $ . . . # nothing
  $ . echo hello
  hello
  $
You can write a function in Bash like this:

  .(){ :; }
This creates a function called "." that does nothing.

Then this is valid code:

  . . .
I typically use [..trim..] or something to that effect. It's a habit I picked up because for people new to programming it's not at all obvious that ... or . . . isn't syntax, and just a word clarifies that (depending on who I'm talking to, I might expand that a bit to [..insert code here..] or some such).
In Python, ... is an expression, so you can do

    def will_implement_later():
        ...
Which I prefer over "pass" in this case because the incompleteness is a little more intuitive.

I think using asterisks for wildcardy-things like variadic args makes more sense too, coming from UNIX land.

(comment deleted)
Ugh, I'm just skimming this but it sounds like quick forwarding syntax in Ruby went through a mess of hiccups in development, which is sad, because I think the obsession with C/C++ as the gold standard of language development for so long really held the industry back. I love languages that offer convenient forwarding - the verbosity of forwarding in traditionalist C/C++-style programming languages is imho a big source of the horrible abuses of inheritance. Such a shame it's mostly provided in languages that are too dynamic-typed for my tastes.
It's more that Ruby has a bunch of pre-existing quirks related to the various things you can pass to a function (positional, named, blocks) and even what functions are (methods, blocks, closures) and that quirkiness creates complexity for simple-sounding things like forwarding syntax.
Why are there so many kinds of things that can be passed to a Ruby function?
You can pass positional arguments because that's how arguments have typically been passed in the history of computing. You can pass keyword arguments because they are much less ambiguous to use when a method signature is changing; you never have any confusion about which method argument is missing.

You can pass positional and keyword args together in a way which is rigidly formalized since Ruby 3.0 because, at one point in history, you could pass a hash, you could pass positional arguments, and you could pass keyword arguments, and nobody was ever able to tell reliably what you were doing:

https://www.ruby-lang.org/en/news/2019/12/12/separation-of-p...

You can pass a block because in Ruby, a block is an object like anything else. And you can pass an anonymous block because in Ruby, the "yield" keyword is an idiom that's commonly used with anonymous blocks. The curly braces and the "do" keyword that are both common idioms in Ruby are nearly identical forms of anonymous block.

The throwaway answer to your question is that you can pass anything that is an object, and in Ruby since anything is an object, you can just pass anything! I haven't read the whole article, but I get the sense that I shouldn't read much into the clickbait headline, since it looks like this is a really thoughtful show or treatment of all the neat handy things that Ruby lets you do with various types of arguments, and not a hit piece! whew relieved

I would call it a thoughtful treatment of how late versions of Ruby solve a problem that well-designed languages never have in the first place.
I intuitively expected this to get downvoted but I'd like to hear opinions of the people who downvoted this, wrt how much of this flexibility is a design flaw (or accidental complexity?) and how much it is expected given the history and evolution of the language.
They didn't comment on what they think good design was so its just pretty uselessly taking a swipe at Ruby. They may be correct that languages made more recently are better designed from the start because they can work around issues that ruby hit because people largely didn't know any better (some contemporaries may have claimed to know better, but there's always people after the horse race who really did bet on the right horse). They may be starting a religious war, believing that something that ruby supports is just so clearly wrong that ruby shouldn't exist. We can't really say, but it doesn't offer any useful comment. And its really easy to say that a 28 year old language shouldn't have been developed exactly the same way, given what we know now.
Javascript and Ruby are of an age, both first released in 1995. Javascript has always supported all of the use cases described here for Ruby, until very recently with exactly one syntactic convention for argument passing. If you want keyword arguments, pass an object; if you want to pass a function, pass a function.

That's simpler to understand because there is less to understand, with these capabilities composed from extant syntax rather than having been invented as one-offs that require being learned and understood as such, each hedged around with its own special cases and failures to generalize - if that were not the case, the article here under discussion and all others like it would never have needed writing in the first place.

I realize describing Javascript as "well designed" will be controversial, and certainly it has not been without its share of flaws: loose equality and IEEE 754 float behavior come most quickly to mind, along with the lack of a general conditional expression. I am comfortable with describing Javascript as better designed than Ruby, though. Certainly it is far more ergonomic.

Good engineering above all else is, as much as possible, simple: it can't help being about as complex as the domain it addresses, but any complication beyond that represents an error of design. Programming a general-purpose computer is an arbitrarily complex domain, which gives all the more point to the need for simplicity in design: the user of the tool has enough to think about already, and should insofar as possible not also be forced to manage unnecessary complexity in tooling. Under such a metric, that Ruby needs three kinds of calling conventions to accomplish what Javascript does with one is already enough to demonstrate that Ruby's design is lacking.

The ergonomics of passing anonymous functions in JavaScript as originally released was awful, as was passing "keyword arguments" as objects. That's why arrow functions, async/await, and object destructuring were added to the language years later. 1995 JavaScript didn't even enforce function arity!

A language that is self-evidently superior due to simplicity and ergonomics would not inspire the publication of a hugely successful book that promises to show people the "good parts" of the language. The superior language you're describing should only _have_ "good parts".

This gets us a bit into the definition of "sugar", I think. Specifically, all the constructs you mention are syntactic shorthands for the same underlying concepts, and the original syntax also remains available - which is to say, those constructs actually are syntactic sugar. Ruby having three ways, in two cases, to do one thing, is not.

I'm glad you mentioned "The Good Parts"! Crockford published in 2008 and the state of the art in JS has so evolved as to make further such work mostly unnecessary, or so I infer from its more-or-less absence over the last decade or so at least. That Ruby still requires such explainers be published in 2023 seems more an argument to my point than to yours.

You're also very anxious to make the perfect the enemy of the good here, which is a surprise after the explicit disclaimer in my prior comment. Why are you asking me to defend an argument I've been at such pains to make clear I'm not advancing?

> the state of the art in JS has so evolved as to make further such work mostly unnecessary, or so I infer from its more-or-less absence over the last decade or so at least. That Ruby still requires such explainers be published in 2023

async/await was introduced in 2017. If you're suggesting that this required fewer "explainers" than a set of gradual changes to Ruby method call syntax, I'm afraid we're not operating under a common understanding of reality.

Kind of funny -- if I understand this correctly -- that Javascript was descended from Scheme which has delimited continuations and yet Javascript has async/await and function coloring nonsense, while ruby has continuations and fibers and doesn't need to worry about "function coloring" or "callback hell".

(Which I don't understand very well because I was a ruby programmer for 12 years, so missed all the async/await/callback-hell nonsense entirely that everyone is so obsessive about these days)

I don't believe Scheme as standardized has delimited continuations, only call/cc; it's just that Scheme implementations have often been the laboratories in which experiments on delimited control have been performed.
Simplicity is why I detest the Javascript syntax and love the Ruby syntax. I spend far more time reading code than worrying about very simple rules for how to write the arguments, and so readability matters far more.
I didn't downvote it, but I don't like the swipe of ruby not being a well-designed language.

One thing to consider: Ruby is old. Ruby stems from a time when "Everything is an object" and "You can pass a code block to a function" are borderline revolutionary. Ruby comes from a time of very imperative PHP, C and such. Python was another language during these early days, as was perl.

And ruby has strayed into a few paths of ambiguities. Like, hash as an arg vs kw-args, blocks, references to blocks. Quite a few things are weird there. Tolerable, moving into better spaces, but certainly weird.

But at the same time, have you looked closely at Java Generics? Or, Pythons multiple inheritance? Or, Perls Object Orientation. Or, PHPs types, until recent versions. If three of us get together, we can start making fun of every language for taking a wrong step, I'm sure.

If I found myself defending a language by comparison with Perl, I'd be nervous. I worked in that language for most of a decade, and there are many reasons why I refuse ever to touch it again for longer than required to write a shell oneliner.

I didn't work with Ruby for nearly as long as that, not least because I found it to share too many of the same problems. I understand why the few people who deeply appreciate it do so, and I don't really judge them for that, but a wider and less partial perspective is also needed.

Then, too, nothing here actually defends Ruby's design per se. That nothing better could be expected at the time is really as close as we get, but as I discussed in more detail on another branch of the thread, contemporaneous Javascript suffices to dispose of that claim. That there are less well designed languages than Ruby I freely grant, but that's also not much of a defense.

I'm not even defending ruby too much. I used ruby for many years and it always felt somewhat off.

However, with the brevity of original comment, it feels like Ruby is getting a lot of flak for shaky design decisions.

While in reality, a lot of languages from that period had very shaky design decisions. Newer generations of languages or better versions of these languages - built upon the lessons of that period - avoid these mistakes and put the spotlight on these "obvious" design issues.

In 43 years of programming, it's by far the most readable of the several dozen languages I've used.

That I routinely can convert code from other languages and end up with something far smaller and more readable is another major plus.

That is all I need to defend its design.

The language didn't have keyword arguments at the time it was originally created (nor did many other languages). The use of hashes as a way of simulating them was popular at the time (I've seen the same in Perl) but it was a good idea to eventually make them a full-fledged feature. It took a while to complete that transition without causing a serious backwards-incompatible disruption. Ruby has generally favored avoiding such things, which sometimes means finding some inelegant solutions.
> it was a good idea to eventually make them a full-fledged feature

Why so?

Because it provides a clearer way of enforcing calling conventions. Calling a method with the wrong keyword argument raises an error automatically. Calling a method with hash arguments the method expect replies on the method manually checking for unexpected keys. In Ruby as in other languages it was error prone.
Positional arguments creates a dependency on the position of the argument. And this is a cause of bugs when calling a method with the arguments in the wrong order.

Keyword arguments remove the depedency on the position.

in my opinion, Ruby (and also aforementioned perl back in it's era, but this would be pointless to defend) is one of the languages where syntax and semantics looks like they took order of magnitude more thought than most of other languages.
There's only 3.

Named args, unnamed (positional) args, and a special block. Because Ruby was basically "let's make Python but without Guido's hatred of metaprogramming", so it copied Python's *args and **kwargs and then added the block so you could make functions that look like control blocks.

To me the problem is that they're handled as 3 vars instead of 1 object that has 3 public properties.*

[flagged]
Assuming you're not talking about values, there's really two things which can be passed to a ruby function: normal arguments and blocks.

And arguments may be passed either positionally or by name.

Blocks are the weird part, they have special support because they're a bunch of syntactic sugar / magic, which doesn't even have to be part of the function's prototype e.g

    def foo
      yield if block_given?
    end
"yield" will invoke the implicit block, "block_given?" is a metamagic kernel method which checks if the current context was passed a block.

I haven't kept up to date with Ruby so I don't know if that style is still en vogue. The equivalent "explicit" version is

    def foo &block
        block.call if block
    end
in which the block — if passed — is reified to a Proc, which you can manipulate normally. If no block is used, then `block` will just be `nil` (as the essay notes, blocks are always optional).
These two forms are equivalent in modern Ruby, but they weren't always. The explicit &block required a heap allocation to close over the block's local scope. This is a "reified" Proc object. The implicit block could remain on the stack, since a block never outlives the stack frame that defined it.

Fun fact: Both yield and block.call may never return. The block is free to return from its enclosing method, which effectively pops multiple frames, including foo and possibly more, off the stack.

> The block is free to return from its enclosing method, which effectively pops multiple frames, including foo and possibly more, off the stack.

As an aside, this is one of the key differences between a lambda and a proc. Last I checked, you can pass a lambda as a block, but it is implicitly converted to a proc.

I'm glad you mentioned that! The existence of three distinct abstractions over the concept of function values is another good example of Ruby's flawed design.
Unlike JavaScript, which only has, uh, three (functions, arrow functions, and methods).
No, Javascript has functions. That arrow functions handle the "this" binding specially I'll grant as a potential design error, and certainly as a complication; to reiterate, I have not said Javascript is perfect. But classes and methods are sugar over prototype-based objects from 1995, which you are still perfectly free to write if you like.
> That arrow functions handle the "this" binding specially

It’s more the opposite, arrow functions treat `this` as a regular lexical variable, it’s normal functions which special-case `this`.

They're not three different abstractions.

The distinction between a block and a Proc is an implementation detail - an implementation can choose to always make blocks Procs (my experimental Ruby compiler does just that) and a program shouldn't be able to tell the difference.

The distinction between proc and lambda with respect to how returns are handled provides significant utility with very low added implementation complexity that for the most part work together to do what people will expect of them.

That is, the most logical reading of the block syntax is that a return will return from the function it is lexically in, so it does, while a lambda looks like a function, and acts accordingly.

If anything, to me these are prime examples of what makes Ruby a great language that values developer happiness over pointless exercises in purity.

It’s really just positional arguments, keyword arguments, and block, in that order, all optional.
I love these series and agree. Ruby was already very expressive language and IMO few major versions ago was a good time to reconsider this area of development in favor of something else. I think contributing to <<optional>> static typing would have had higher RoI...
Programming languages shouldn't have two competing two type systems in them. Static typing in a scripting language like Ruby is just poor engineering.
Static typing for JavaScript hasn't turned out to be poor engineering - it helps massively and has become the de-facto norm. Similarly, static typing in Python is a massive help and keeps getting better. Why would Ruby be any different?
Because JavaScript is the worse programming language in common commerical usage today. It was designed over several decades by browser manufacturers.

Absolute anything else would be better than it.

Static typing in Python is a complete disaster when it's used on real commerical projects. I've seen the code thank you very much.

> Because JavaScript is the worse programming language in common commerical usage today. It was designed over several decades by browser manufacturers.

Do you mean "the worst"? There are plenty languages that are worse, and that are still in use. It's not great, but it's also not as terrible as you're making it seem.

> Absolute anything else would be better than it.

Okay, what does that have to do with static typing? Or are you saying that Typescript is better than Javascript? If static typing can make such "the worse" language better, why can't it improve other languages as well?

> Static typing in Python is a complete disaster when it's used on real commerical projects. I've seen the code thank you very much.

You make it seem like the alternative, not having static typing, is in any way better. No, it's definitely not - having e.g. your IDE know the types of values to help you in refactoring is extremely valuable, as is having static type checkers tell you about issues before running your code.

You either have unknown type problems because you're missing test cases, or you're re-building a static type system in your test suites. There is no way around verifying your assumptions about types if you want to build stable software.

Static typing is a very bad software development practice. There are a lot of other software development practices that are superior to it.

"help you in refactoring" It's a terrible way to refactor your code.

There are far better and far superior software development practices out there, which do everything static typing does and more.

"There is no way around verifying your assumptions about types" Yes, there are. There are many many ways to do this that do a lot better job than static typing. Static typing is known to be terrible at eliminating bugs from code.

The complain here is not that static typing isn't better than nothing but that static typing is such a low tier solution, that if you were doing anything proper you wouldn't be touching it.

> Static typing is a very bad software development practice. There are a lot of other software development practices that are superior to it.

Can you give some examples for these supposedly superior techniques?

> "help you in refactoring" It's a terrible way to refactor your code.

Could you explain why you think so? Say I'm changing the name of an attribute. Why is using static typing to find the places I have to change terrible?

> "There is no way around verifying your assumptions about types" Yes, there are. There are many many ways to do this that do a lot better job than static typing. Static typing is known to be terrible at eliminating bugs from code.

I didn't say what you think I said, since I didn't touch on static typing in this. Want to try responding to my actual argument?

Type systems aren't competing, it's usually people fighting over them and it's no different in Ruby
Typescript is so good I'd love to see Python, Ruby, PHP, etc. and every other dynamic language steal at least half of it. Python already tries, I think it was typescript's early success which tipped the scales from 'type hints are whatever' to what's there today.
Sorbet is pretty good in Ruby, there are rough edges but for that you have good escape hatches so you don't have to work full time for the type system
In my experience, in an RoR codebase, the dynamic-ness of rails severely reduces its utility to only enabling better auto-complete (as opposed to also helping out with writing logically correct code).

In typescript, when I do a refactor, or I add a feature, I will code for hours, relying solely on the typechecker for feedback, and then I will manually test, and then write unit tests. (It helps that I use discriminated unions with exhaustiveness-checks, and avoid shortcuts like `any`, to the extent possible).

Such a workflow is impossible with how limited sorbet is in the RoR context.

I'd still choose an RoR codebase with sorbet than without it any day of the week tho.

In my experience Sorbet is a lot less useful than mypy and frequently unhelpful or useless, and both of those aren't in the same league as Typescript.
That ranking of type systems matches my experience a 100% too.
PHP already has an opt-in type system that's runtime safe (ie it's not just compiled away like in TS).

The major piece of the puzzle missing is generics support, but other type handling is already there.

>Static typing in a scripting language like Ruby is just poor engineering.

I agree, but in that case, using a scripting language to build and maintain applications with significant LOC ... is poor engineering.

Static typing is there so you can get compile-time support for validating assumptions made throughout your source code, which is critical for developing and maintaining LARGE applications by teams of engineers of all skill-sets, over significant periods of time.

"maintaining LARGE applications"

And that is very poor software engineering. You should be breaking down those large applications into smaller more maintainable micro-services.

It's bad really bad.

"compile-time support for validating assumptions"

Bad software development again. If you are checking assumptions at compile time then you are slowing down your software development iteration loop. It's the 4 hour compile time problem from C++ revisited.

"over significant periods of time"

Bad, really bad software development again. As business requirements change over the life-time of a software project, you need to retire and replace old code with new code that better meets the changing business needs.

Static typing is useful for performance reasons but that doesn't apply in a scripting language. In pretty much every other context it's terrible.

I can't tell if this is a troll post or not.

>You should be breaking down those large applications into smaller more maintainable micro-services.

This take ... I'm not even sure where to start: 1. Everything I said applies equally to microservices as it does to monoliths. 2. You must have never worked with microservices if you think they are a panacea. 3. What is the difference between the same amount of code but split between one monolith versus many microservices?

>It's the 4 hour compile time problem from C++ revisited.

Do you know what's even more expensive? Tracking down TypeErrors at Runtime.

> As business requirements change over the life-time of a software project, you need to retire and replace old code with new code that better meets the changing business needs.

Yes .. keep rewriting working code - that's a path to success.

"What is the difference between the same amount of code but split between one monolith versus many microservices"

Encapsulation at a larger grain level.

"Yes .. keep rewriting working code - that's a path to success." If you had done any courses on software requirements, you wouldn't be saying that. It's the entire premise behind agile software development.

> If you had done any courses on software requirements, you wouldn't be saying that. It's the entire premise behind agile software development.

Oh really? Twenty years of professional experience say otherwise. I don't remember if that came up in either my graduate or undergraduate classes, but I also remember this post going around: https://www.joelonsoftware.com/2000/04/06/things-you-should-... - in my experience, this post is as true as it ever way. Sometimes a ground-up rewrite is necessary, but more often than not, you do it at your own peril.

>Encapsulation at a larger grain level.

Encapsulation has nothing to do with it. The value of compile-time support rises as LOC and complexity goes up - this is irrespective of your development methodology, or the architecture of your stack.

>It's the entire premise behind agile software development.

No. It's not. Agile does not directly address code maintenance or code quality since Agile is (as Wikipedia puts it) "an iterative development process". You can perfectly follow Agile, and turn out unmaintenable code.

(comment deleted)
Work on optional static typing for Ruby has been ongoing for several years, with type signatures being added to increasing amounts of libraries.
The author goes deep in explaining not only the technical details, but also the motivation why things were crafted in a certain way. Actually all of his articles on Ruby are like that, personally I like it a lot
I’ll take “how to make code slower and more error prone in order to save a few keystrokes at the cost of readability” please.
Really, it’s because of the habit of Ruby programs to pass a single options (params, args, config) object in to avoid complex configuration.

Really, what’s needed is separation of concerns, instead of a single “do stuff” function that takes an “everything” argument.

But what’s really wanted is global variables with everything is a single scope.

That’s certainly a valid perspective.

Another one, “how can we make a powerfully expressive programming language feel malleable, accommodating and pleasant in the hands of experienced devs?” I equally valid.

As someone who writes Ruby I appreciate the language every time I use it. There isn’t another language that has given me such satisfaction - so far. Most accommodate the interpreter / compiler. This one accommodates humans.

(comment deleted)
Readability is a function of the reader, not the code. It's a dog-whistle that just means "familiarity".
How does this syntax make code slower and more error-prone? According to the article, it actually saves allocations in common cases.
Doesn't Clojure support this kind of variadic pass through?

Admittedly it's a bit more complicated than Ruby's syntax, which I find to be a bit ironic.

I'm not sure if Clojure has something equivalent to &block... I think you would just pass a function in Clojure since they are first class?

https://john2x.com/blog/clojure-destructuring.html

I think Clojure prefers macros for things Ruby uses blocks for. For example, `with-open` is a macro that accepts the "block" as a `& body`, while a similar mechanism in Ruby would just take a block/closure that looks like any other statement block.
Not so, much more typical to just take an anonymous function for things ruby uses blocks for. For example, map, or filter (select in ruby), or reduce.

“with-open” is a macro because it needs to do something more complicated (create a bunch of bindings then close each one created, in addition to executing a fn body where those bindings are available).

Passing an fn is preferred because functions are more broadly preferred in Clojure code to macros, for both practical (more composable) and cultural reasons.

Anonymous functions return from themselves, whereas a block is a special kind of closure that returns from the caller. It's probably less important in Clojure since it is more functional and doesn't use explicit control flow much.
This is the part that fascinates me about Code Blocks so much.

Is it like an extra break or return statement at the end of it that bails out of the context of whatever method it's running in?

Or does it actually return back to the original all site after nested passes of the Code Block?

I am not a rubyist, but a block (which I think is properly called a proc) is different from a regular lambda in that its control flow statements are lexically scoped to the method it is defined in. A return statement in a lambda returns from a lambda (and always succeeds), a return statement in a block returns from the method the block was defined in (and fails with a LocalJumpError if the call frame is no longer on the call stack).

I think Kotlin has a similar distinction between lambda expressions that return from the definer (and it ensures that you're passing them to an inline function that won't save the closures for later) and anonymous functions that return from themselves.

Typically, you can get the syntactic effect of Ruby blocks in a lisp using macros. Clojure tends to be a bit more anti-macro than other lisps, though and so this is less common than it would be in CL or Racket.
That makes sense, but isn't the Ruby &block evaluated dynamically?

Lisp macros are expanded at compile time.

From my limited understanding this would seem like a dynamic, run time, Lisp macro.

But I don't know if the &block actually operates on the syntax. It looks it doesnt.

From my research, it appears that blocks are more like a lambda that has an extra return statement bolted onto it.

It also looks like you can pass them into methods to significantly alter what the method does, and how it returns.

Pretty fascinating pattern. I'm not sure how I would implement something similar in Clojure.

Maybe higher order functions, with lazy sequences combined with predicate checks.

Ruby blocks are just closures. They’re not macros as they don’t operate on syntax trees. Perhaps the person you are replying to was stating you can emulate some aspect of block syntax with a macro. If not, not sure what they’re saying.
The reason people like blocks is that they give you a nice way to embed DSLs in Ruby code. The fact that they’re “just closures” seems like a secondary thing in most uses I’ve seen of them.
There’s a pattern called the call-with pattern that gives you this dynamic behavior of blocks. But, in many cases, Ruby and JS use this sort of dynamic “macro” where there’s not really a benefit to dynamic evaluation: with a macro you can always wrap the block of code with an anonymous function at macro-expansion time and opt-in to dynamic evaluation; Ruby blocks (and arrow-function DSLs in JS) force you to pay the overhead of dynamic evaluation even when you only need compile-time information
I recently stumbled upon something in JS (specifically NodeJS and a few other runtimes) that might be of interest here: AsyncLocalStorage (https://nodejs.org/api/async_context.html). It allows for storing data specific to the current asynchronous context. I've been using it primarily for adding logging prefixes and managing authentication in Postgres with Row-Level Security (RLS).

Essentially, it offers a server-side alternative to "prop drilling," where you repetitively pass the same parameter through a chain of function calls.

C# has the same thing and I use it for the exact same things
wait, what? can you point to a feature name / doc / example?
This is extremely similar to thread local, a feature in many languages including C#.
I'm pretty sure they were talking about AsyncLocal<T>, which is not quite the same as ThreadLocal (values are persisted even when an async flow transitions across).
Yes it’s different but conceptually similar in the sense of ambient execution context.

I prefer implicit variables like scala has but it gets the job done in C#.

They mean AsyncLocal<T>, which ties its storage to the current thread-local ExecutionContext. When await schedules a continuation, the ExecutionContext flows with it and is set before resuming, so the AsyncLocal values flow through async methods even if the task switches threads.
This type of problem seems to be the exact reason why Object Orientated programming was developed in the first place. Whenever I'm repetitively passing arguments from one method to another, I usually find an object hiding in plain sight.
At some point you’ve got to stop messing with syntax. It plain sucks not being able to run anything on a linux machine that is on a LTS schedule. The constant churn of version updates that fix some things but break other things is a massive time waste.

It’s practically impossible to remove features from a language. If you keep adding “improvements” to your language you end up with the unholy mess that is modern c++

Isn't the whole point of LTS to outsource all this nonsense to the maintainers of the distro?

Back when I was heavily involved in blender development I'd sometimes have to pause my hackery while waiting for fedora to catch up with whatever version of python blender was using because I didn't want to bork my system by upgrading python (again!). Well(...) I also wanted to ensure that it would run off libs from the repos and this was a little higher priority than being on the cutting edge so it was a sacrifice I was willing to make. The only time this caused any significant delay also coincided with one of the open movies and they didn't want to merge what I was working on until after production which meant it didn't matter either way.

I don't ruby but it seems like a nice way to do something like:

  def apply(callable, ...):
      callable(...)
In practical terms, if you're using the distribution's interpreters for anything other than running code supplied by the distro, you're going to be in a world of pain. That is the whole point of a distro: to supply a known-good set of mutually compatible software.

Expect to be building your own environments if you want to run things that are not in your distro.

I can't relate. I'm a Python programmer and I don't think I ever used a Python version other than the one provided by my distro. Unless you run a very outdated/"stable" distribution, interpreter versions are no more than few years old. Most serious projects don't just jump on the new language version on the day it's released, certainly not my projects, so I never had the need to run a bleeding edge interpreter.
I think that the GP has hit on something deeply true (distros are mutually compatible bundles of software which aren't guaranteed or even intended to work with your dev environment). I think it is also true that this is not usually a problem for Python in practice for small-to-medium projects.

I will note that most places I have worked professionally with Python code have compiled their own CPython interpreter and not relied on the distro version.

> Unless you run a very outdated/"stable" distribution

LTS is exactly this pain!

I really don't understand why anybody would use that for development. Your SW won't land anyway in the current "stable" distri release, but at the earliest in the "next" version. So you need to develop against that "next" version anyway. The "next" version is something like Debian Testing…

And if you don't plan to release your SW properly into a distri you need to bring your own environment anyway.

First of all, don't run anything "LTS" on a dev box. You really want a "rolling release" for that purpose. Something like Debian Testing (as it's the most compatible).

Secondly it's very well possible to remove features from a language. It just needs to be well enough designed that there is a good migration story (ideally though automatic rewrite by the compiler). Just see what Scala 3 did!

https://docs.scala-lang.org/scala3/reference/dropped-feature...

> don't run anything "LTS" on a dev box. You really want a "rolling release" for that purpose.

No. Give the same environment as production.

You don't run your dev tools on the production boxes.

And you don't run your apps directly on the production boxes either these days.

There is just no reason to endure the constant pain of outdated dev environments.

Of course that doesn't mean that you can't develop against a stable environment, in case you plan to deploy directly onto such thing. That's usually independent of your tooling. You just need to look for the right versions of your deps (which is trivial within some "container"; which runs of course on your up to date dev box).

I didn't say run on the production box. I said, create an identical environment as the production box.
This (syntactic sugar) is the kind of thing that wouldn't even be considered in Go. And that's fine.

I find it interesting that Go and Ruby can be placed at two different ends in so many spectrums and yet both still managed to be popular.

Goes to show that trying to build THE ONE TRUE LANGUAGE THAT WILL UNITE ALL DEVS is a fool's errand.

I think that Javascript's massive success with the terrible syntax it used to have goes a long way toward confirming your hypothesis.
“Used to have” :)
Haha yeah, I was being charitable to avoid starting a flamewar ;)
Do people do real work in JS in 2023, or do they do it in languages that compile down to JS?
Ignoring compiling and polyfilling for older JS versions, Typescript is extremely popular. While it's compiled, all things beside the type system are from Javascript by design including syntax.
Probably of all people a lot of ruby acolytes do since a sizable portion of that community is a DHH personality cult. And plain JS is the 2nd best language ever made in that realm.
JavaScript's massive success has nothing to do with the language and everything to do with the fact that its embedded in every web browser. There were no alternatives.
That's like saying "Objective C was a broadly successful language"…
Even more interesting to me is the fact that there are people whose most favorite languages are Ruby and Go. (I'm one of those people.)
Same. Ruby is easy to read. Golang is super efficient and much safer.
(comment deleted)
Migrating some projects to use … was a pain but it is a better pattern imo. Rspec expectations around arguments can be tricky.
I tried picking up Ruby some time ago, when RoR was at its popularity peak and seemingly everyone couldn't shut up about how “elegant" and fun Ruby was. After trying it... I can say that it's for sure someone's idea of elegant code, and I can see why many people like it, but it just didn't resonate with me at all. Too many syntax quirks that didn't look particularly nice to read IMO.
It’s funny because I think overtime this is the same sentiment that seeped into the Ruby community. It used to be with pride when something worked with ‘magic’, but Ruby and even rails have both gotten away from that mentality, and I think for the better.
Ruby's weird for me in the sense that it is the language that most directly maps to my thought processes. Ruby's syntax feels extremely natural, but I first learned Java and C like a lot of my other cohort. Maybe it's because the first programming language I was truly good at was Perl
Ruby has the highest ceiling for clean, elegant, and expressive solutions to problems in any language I’ve ever seen. For small teams of (one or two) highly-experienced programmers with compatible vision, you can produce truly incredible programs.

It also has the lowest floor. With the usual makeup of teams of frequently-rotating mostly-junior engineers who don’t have the experience, understanding, and restraint to use Ruby’s more… let’s call them idiosyncratic features, the sheer volume of completely inscrutable crap that can be put out in a given amount of time is truly breathtaking.

I feel the same way and I'm so happy I gave the language another chance after getting turned away by the behavior/attitudes of the Rails community back in "The Day". Ruby made me love programming again :)
This is how I feel with Clojure -- it fits into the way my brain thinks about how to solve problems computationally better than anything else.
Yes there are definitely some weird syntax quirks, but it all depends on your perspective. Coming from something like Java, C# or Typescript, the syntax indeed feels strange, but from other languages with similar idioms (like Python), it's a lot less weird. Some examples of Ruby's quirks: `elsif`, singular `include?` instead of `includes?`.

But the benefit of RoR is not really Ruby, its Rails. When you embrace the convention-based approach of Rails, you can move very quickly. A lot of the evolution of Rails over the years has been making it possible to have that cake and eat it too; being able to override the conventions when needed while the conventions keep the boilerplate you must write to a minimal level. And ActiveRecord is by far the best ORM I have ever used.

That being said, some of Ruby's design considerations are important to why I still enjoy Rails development: being able to control your system manually in a fine-grained way from the Rails Console by interacting directly with models is really lovely, and the power of that isn't really matched by any other language+framework combination that I'm aware of. This ability to call directly into your model layer really requires a global namespacing arrangement (unlike for example JS) which is not too deeply organized (like in Java), with syntax that makes calls into the model layer not too onerous (ie omitting parenthesis for method calls, because every single `.` call on an object is a method call in ruby).

I wouldn't necessarily pick RoR/Ruby for new projects myself, as Typescript's IDE-time erasable static typing is the number one thing that I wish Ruby had (there are several initiatives to bring over these benefits but they are still nascent), but I have no problem collaborating in teams working on Rails projects as it is indeed a very enjoyable, very mature platform for building APIs, backend systems, and even customer-facing frontend systems.

> singular `include?` instead of `includes?`

Pedantic tangent, but `includes` is actually the singular form: "This array includes that object" vs "All of these arrays include that object".

huh, true. Still backwards in ruby :-)
No, it's a question, "does array x include object y?" `x.include?(y)`
(comment deleted)
> In a radical variant of this, Perl’s sub doesn’t have a syntax for arguments declaration at all

Actually, Perl has had signatures <https://perldoc.perl.org/perlsub#Signatures> as an experimental feature (requiring `use feature 'signatures'`) since 5.20.0 (May 2014), and they’re considered stable (requiring `use v5.36` or later) since 5.36.0 (May 2022):

  use v5.36;

  sub test($x, $y) {
    print "x=$x y=$y\n"
  }

  test(1, 2)
  # prints x=1, y=2
Oh, thanks! Added a footnote to the article.
As a lifelong rubyist, this is definitely what I would say "useless". Maybe it's just because I've been stuck in Ruby 2.6 ~~hell~~ land for so long but I remember seeing a colleagues leetcode challenge for a job interview that used Ruby 3.1 and he used a bunch of these new syntax sugars and I was really rubbed the wrong way by them. Code the omits arguments is neither more readable (what exactly is getting passed around here?) or maintable (what happens when you have some condition that may apply to one of the arguments, then you just need to unwrap all the "nice" things and do it more traditionally).

The elipsis ... especially rubs me the wrong way because multiple periods either .. or ... denotes range types, but now we have a special case where ... is something completely different. Bah humbug.

They seem nice in clean blog scenarios with trivial examples, but in my experience they are not conducive to writing readable and maintable code.

Thankfully I can just disable their use in my projects (for the most part) with rubocop. All the hip young kids can write their magic code, I'll stick to more straightforward constructs that anyone can figure out.

(comment deleted)
(comment deleted)
(comment deleted)
I’ve been using Ruby since 1.7 and think the new syntax is fantastic. I hope you’re not weaponizing Rubocop in team or community projects in a manner that discourages “hip young” Rubyists from learning and exploring new ways of pushing the language & runtime forward.
We have developers of various experience and skillsets that come and go, sticking to a subset of language features with minimal magic has made it easier to onboard and get people productive. We have massive codebase, and sprinkling these new sugars around in arbitrary places to be lindmines for them isn't worth it to me. From a business perspective, we're not too concerned about pushing the language forward, we're focused on creating value for our customers - the language is just a detail.
If the language is “just a detail”, then why go through the trouble of writing about your distaste of new syntax features in Ruby? Using new language features does not categorically hurt or help “create value for customers”.
Not the originator, addressing only your question, the hacker news commenting system is a form social media. People are free to comment as they see fit and for reasons that make sense to them, within certain guidelines that don’t appear to apply here. My impression is your comment, and previously theirs, is an airing of grievance. Which is common and par for the course in social media spaces. My intention in commenting is to give a voice to what is typically an unspoken understanding.
It actually really helps with maintenance.

If you have a wrapper for an API (say, that does some logging or whatever), you can use `…` to forward arguments, maintenance free.

Perhaps the wrapped function started out with no arg, but then added one. You never took/forwarded a `*vargs` array, so now you have to manually update that.

Then perhaps some other function started taking a block. Now you need to add `&block` and forward it.

So `…` is a maintenance-free way to forward arguments to functions that are free to change.

Love it. Anything that makes Ruby feel more like Smalltalk and less like Python is a win.
I have no objection to your opinion but * and * are somewhat.. Python-inspired
* and *

stupid formatting

This article make it look like understanding Ruby code has become an absolute hell.

I don't mean understanding as in "I get what this demo snippet is doing kinda", but as in "what is exactly happening here, how to edit it, and what might break if I edit it".

Feels like the antithesis to Perl's write-only code. Ruby is read-only code.

When is the last time you used Ruby?
(Author here)

I don't think this corresponds to my experience.

I am working as a staff engineer in big production codebases (with all the traits of those: some code is good, some is bad, some is unrecognizable legacy, sometimes we are in a hurry and write awful code, sometimes we have time for refactoring etc.)

And my observations about the logic and perception of the features are drawn from the practice of code reviews, mentoring new people, and discussing ways of solving tasks in this environment. Obviously, I am frequently a driver of new code practices, but I am also trying to be a good person and a good colleague and notice how comfortable people are with various parts of the codebase, various idioms, etc.

One of the main topics of this article series is uncovering the language's logic and intuitions (to stop perceiving it as a "bag of random syntactic features you need to learn or should guess") and using those intuitions for code that is, yes, better for the reader, but the code that can be created in a quickly-changing production environment and rewritten fearlessly.

I'm not objecting to your article, but to this language and its (you called it!) "useless Ruby sugars". People sometimes use humor to mask suppressed pain, I've noticed.

A smart individual can adapt to any environment and its quirks and thrive. But some of their capacity always goes towards serving the "quirks adaptations". It's kind of like GPT. It's a smart bot, and if you ask it to answer a math puzzle by writing backwards in Romanian haikus - it will. But the effort to make haikus in backwards Romanian make its math puzzle answer that much more likely to be wrong.

I have no shadow of a doubt that people who are used to Ruby know every shade of gray in how to forward arguments through byzantine syntax. Much like PHP programmers have zero issues with every function in stdlib randomly switching needle/haystack parameter order, or remembering which identifiers are case sensitive and which aren't, and the mix of conditions where an identifier needs a $ in front or doesn't need it. Or which one of 10 different stdlib APIs doing kind of the same, but in different state of deprecation and introduction they should use. Or the fact PHP has a type system, but not for the most common data type (array), and there are two modes of validating parameters (strict and non-strict) which can be switched on and off per file and bomb on you unexpectedly when combining both, despite your code is logically correct in isolation. It's trivial stuff to them. They can juggle all this and more. But someone coming from outside would take one look and say "how about no". Because PHP for all its advancements is not a good language.

And Ruby is also not a good language. In fact, it's clearly getting worse over time.

Here is common use case I use for this in my Rails apps:

This is the most common way I use it:

    class ApplicationService
      def call
        raise NotImplementedError
      end
    
      def self.call(...)
        new(...).call
      end
    end
    
    class MyService < ApplicationService
      def call(service_specific_arg: 123)
        #...
      end
    end
I’ve been using the same structure for years :)

But I prefer to replace ‘raise NotImplementedError’ with ´raise “Not Implemeneted”’ because that exception has different semantics (method unavailability on the current platform)

I also avoid `NotImplementedError` as it inherits from `Error` instead of `StandardError`. This can bring down services like job queue processors that rescue `StandardError`.
What is this code good for?

I can understand what it does but I don't get the intend behind that. (I'm not a Ruby programmer, so maybe I miss something "obvious")

ANSI CL:

  (defun wrap-foo (&rest args)
    (apply #'foo args))
TXR Lisp:

  (defun wrap-foo (. args)
    (foo . args))
The (. args) syntax (consing dot not preceded by item) is equivalent to args; if we write it that way, we are not using any syntax that didn't exist in 1958.
Ruby is a truly hideous language that keeps getting uglier by the year.
I love the ellipsis. Ruby has made so many good choices as it moved into 3 and I’ve only been happier and happier with it.

I also write my Ruby Seattle style, so I’m one of those weirdos.