Naming things properly is a skill we all need to have as programmers. There are (multiple) reasons we don't call things k, d, j, z, o all over the place anymore.
And for the record, I actually prefer λx. λy. x instead of λ λ 2. We do not need to be as terse as possible when writing programs or mathematical statements. This is especially egregious for Ruby, a language that has been valued for being quote, "elegant".
a =
some_list_of_tuples()
|> Enum.map(&elem(&1, 1))
|> # more pipeline here
The alternative, without De Brujin indices, is: a =
some_list_of_tuples()
|> Enum.map(fn x -> elem(x, 1) end)
|> # more pipeline here
Are you going to tell me that the second example is more readable? Personally, I find that the tiny little elem/2 expression gets lost in the line noise of the closure syntax. Reducing that syntax using an "anonymous closure" like in the first example makes it clearer to me what's going on.And, as well, in such closures, there really is no "name" for the thing that I'm processing. It's an intermediate in a destructuring expression that I'm only holding onto in order to further rip it apart. (The output has a name, say, `foo`; but if the input is just, say, `{:ok, foo}`... then what is the name of said tuple? `result`? `maybe_foo`?) Whatever name you make up there, it would only distract a future reader by making them think that it might be something important to the domain.
Oh, and also, at least in Elixir, De Brujin anonymous closures "flow well" with function handles:
&List.first/1 # function handle
&List.first(&1) # equivalent anonymous closure
---But to put a finer point on it, in practice, 99% of the use I get out of (De Brujin) anonymous closures is just using them as "function handles but with the parameters reordered"—i.e. as a way of wrapping a closure in a combinator, without having to remember the names of combinators.
Instead, with De Brujin indices, such "combinator" effects are self-evident descriptions. Elixir code:
&(x.(&2, &1))
What is that? An expression that returns a version of the closure x (of arity 2) where the two input parameters are swapped. In other words, that's the C (cardinal) combinator, or the Haskell function `flip`. Except you don't need a special name for it; you just describe the effect you want then and there. Anyone who reads it can see what it does. Is the non-De-Brujin equivalent any clearer? fn a, b -> x.(b, a) end
&1 and &2 are metasyntactic variables. a and b here are also metasyntactic variables. There might be a better name for what they are, but frequently there isn't, if this code is e.g. sitting in a library that does something generic with a data structure or algorithm, rather than something specific with business logic.Personally, if I'm going to be using metasyntactic variables anyway, I prefer to use ones that look different in a way that highlights them as metasyntactic variables. Just like I prefer languages that require some sigil on class-instance variables in a method to differentiate them from lexical variables. It allows both regular syntax highlighting, and the "syntax highlighter in my brain", to work more efficiently.
Yes, this is actually more readable to me (a non-Elixir user). This syntax is explicit about what is happening.
> fn a, b -> x.(b, a) end
Yes, this is more clear!
I'm not going to discuss whether these are good or bad in the context of Elixir, but I do think the former examples are much more clear than the ones with the indices. Since Elixir uses arity notation it makes more sense, at least.
These have no place in Ruby, however.
Sometimes you haven't decided what a name should be yet. I do not want to see @1 @2 @3 in code 6 months later, either. That's the kind of thing that I'd search for in my codebase before I closed the project, and assign a name before it's too late for me to remember what that bit of code was for. (And look, how handy it's a completely original arrangement of characters that you can even grep for without straining!)
But sometimes you don't have a name ready and it's actually better to defer naming the thing until you know what it is. This feature enables that. You can save yourself from picking a wrong name, come back later when the code around that @# has solidified and it's clearer what to call it. I have never seen this feature before this article, but seeing the ways that people are arguing against it has given me a better understanding of the ways that it can be used well.
I've just been catching up on my tech lore and I'm watching Sandi Metz talks from RailsConfs gone by. There's one talk in particular "all the little things" where the subject is the gilded rose exercise, and how she would refactor it. In the course of the exercise the code complexity goes up (almost doubles) before the gains are realized, and suddenly there's this huge bit of code that can be safely deleted, now the complexity has gone way down and the code is good.
My only point in bringing this up is that there are intermediary states that, if observed in a vacuum, we'd all agree are badly formed and simply not exemplary. This strikes me as an example of one of those features. You can come back and make it better. This is just how it will look "on the way" to something better. (I know, "you won't come back" but that's not an argument that I'm ready to accept.)
You say:
> sometimes you don't have a name ready and it's actually better to defer naming the thing until you know what it is
You can do that without this feature; you can use throwaway variable names like "x" and "y".
Taking an example from the post:
> (1..9).each_slice(3).map { |x, y, z| x + y + z }
vs
> (1..9).each_slice(3).map { @1 + @2 + @3 }
The shorthand saves you 7 characters in the rare case when you truly have no idea what to call your block parameters (even a single-character name can be descriptive, eg if x and y are coordinates). That seems minimally useful.
Meanwhile, you've made your block parameters look like instance variables, and everyone else (including all tools that work with Ruby code) has to learn why they aren't.
Seems like a poor tradeoff to me.
This is a strawman and not what I believe. I never said your codebase should always be optimal in every way. I believe @1 instead of even the most basic of variable names is actively an anti-pattern.
> That's the kind of thing that I'd search for in my codebase before I closed the project, and assign a name before it's too late for me to remember what that bit of code was for.
Maybe you might, but plenty of other people will not. I have also lied to myself and added "TODO" comments in some small projects I've worked on. Months and years later I sometimes see "TODO" comments because I ended up working on something else or forgot. It's simply way too easy to not go back and refactor or fix things.
There is nothing more permanent than temporary code.
Sometimes you have to go back and rename things multiple times. This is vastly better than what you know full well will happen: People will immediately gravitate towards the easiest option of @1, and then never change it because they've moved onto other parts of the code or different projects, etc.
We should not be adding ugly, terse syntax to programming languages because it's "just more convenient" to not name things. That is not a tenable argument for a permanent change to a mature language. Being lazy is not an argument.
TODO statements are little lies that programmers tell themselves. "I'll go back and fix this/refactor it, yeah, yeah...". And most of the time, that doesn't happen.
So it was cheaper to leave the TODOs in the perfectly OK code, sounds like you dodged a bullet there.
This strikes me as a slippery slope. Start with "@1 is not an appropriate way to represent this variable" then move onto "I actually don't like the name you've chosen" and finally arrive at "this code isn't important enough for me to take care of those TODOs I placed before I'm old and gray, remind me why are we even worried about how this variable is called, now renaming it for the _third time_?"
I'll usually take a collection of "xs" and iterate over each "x" without any pretense that I'm coming back to make it better. We can agree to disagree, but I'm arguing that it's not any better to do that, and it's actually harder to grep for which makes it actively worse. This is a better option.
My point is that I never went back and fixed it, because often people do not do that.
I, however, can still read that code perfectly fine and well because it has variable names. Not @1 and @2 for hashes, etc.
> now renaming it for the _third time_?"
Things get renamed from time to time (and less than you're making it out to be, typically). It's part of programming. You will be fine.
It's better to be oh-so-inconvenienced than to have to read other peoples' codebases where they just use @1 and @2.
Why have any variable names at all then? Just use 0-999, if it's so annoying to have to think for a few seconds to a minute and pick an apt name.
If I do {@1 + @2} exactly once is that really harder to read than {|x, y| x + y}?
If I'm doing it everywhere all over my code, sure that's a problem and it will make unreadable garbage. But then again it was already possible to make unreadable garbage code.
Usually something is being mapped over, or being selected/filtered etc, with a context, so it's useful to have something like |course|, |report|, or |course_name, course_values|, especially for people-who-are-not-me that may be reading the code.
The issue here is that @1 and @2 make it much, much easier to write garbage code. At least with named vars you explicitly have to acknowledge that you're choosing a bad name. @1 makes it so you don't even have to think about it at all. This is a flagrant issue, and doesn't belong in Ruby at all.
"In real code with actual logic" this would be a "do-block" or you're violating another stylistic rule, and none of the examples I've seen use numbered parameters with a do-block. Is it allowed? I feel like this is important information that I need to know before I can make a fair judgment about whether this is a "flagrant issue" or not.
Do-blocks with numbered parameters, if it is an allowed way to write a block, would definitely be a terrible idea.
But I don't think it is, I can't find a source that says so, since all of the results for "numbered parameters" seem to be rants against the inclusion of this feature. Have you ever read bikeshed.com? Because that's exactly what we're doing right now.
The latter is extremely ugly and does not belong in Ruby whatsoever. I'm not budging on this.
courses.map{|course| course.some_method + some_str}
is unabashedly and unnecessarily verbose, I know it's a course as it's from courses. No need to repeat yourself twice.
courses.map{@1.some_method + some_str}
by comparison is perfectly clear, once you know the syntax.
Edit:
I just installed ruby-2.7-head and I made a typo, first thought it is not permitted in a do-block. It is. I think it's bad to use this in a do-block. But like I said, there are already plenty of ways to make your code into unreadable garbage, it's up to the author to make a good decision. Some programmers will choose poorly no matter how hard you try to prevent it.
In my opinion, this syntax is for one-liner blocks only. You should avoid using tools in incorrect ways, and code that has numbered parameters strewn everywhere without any attention to naming, is guaranteed to have more than just one bad code smell in it.
But I disagree that it's the responsibility of the language to keep potentially dangerous tools out of the hands of developers.
That is pretty much entirely the point of higher level programming languages.
Like preventing you from allocating and freeing memory on your own, because you might screw it up.
Or removing pointer arithmetic.
Or reducing the scope of mutability.
Or preventing access to "private" object variables.
Many programming language features are basically guards to make it less likely you cut your hand off.
Nobody is arguing that there aren't ways to misuse this new feature. There obviously are! But I can tell you we have plenty of time to call the ways out and warn everyone not to do those things, before ruby-2.7 will be affecting any legacy codebases.
If you feel strongly about this, the time to argue about it is definitely now, not after the first 2.7 release is already cut.
It makes the list of un-googlable idiomatic syntaxes that you must learn and keep in a list, in order to guarantee you won't ever find a bit of Ruby code that you can't understand, marginally longer. There are several such features planned in the ruby-2.7-head now. Is this really the worst of them?
It's certainly no easier. "{|x, y| x + y}" immediately clearly conveys "this is a block taking exactly two arguments and returns their sum". Adding syntax for "@1 + @2" adds an entirely new construct for every Ruby programmer to learn and recognize as a pattern, in addition to the full block syntax form, and their brains to be able to switch between pattern matching for either pattern to quickly parse what the code is doing.
All for zero benefits.
String enough of these arbitrarily different syntactic variants for the exact same thing, and you end up with C++.
tuple.collect{ |x,y,z| [x * 3, y * 3, z * 3] }
is better than
tuple.collect{ [@1 * 3, @2 * 3, @3 * 3] }
because with the latter, I need to know in my head that there will be three parameters.
The more I read about this syntax the worse I feel about it.
This is syntactically valid ruby, current state:
tuple.collect{ |x,y,z| [x * 3, y * 3] }
and so is this:
tuple.collect{ |y,z| [x * 3, y * 3, z * 3] }
The last one will generate a runtime error (unless "x" is already defined in the closed-over scope the block exists inside of, in that case it might be valid and correct.)
But both are valid Ruby expressions.
Neither are less inscrutable than:
tuple.collect{ [@1 * 3, @2 * 3, @3 * 3] }
This expression will return nil for @3 if only two arguments are passed in, again it's a perfectly valid Ruby expression, and you'll get a runtime error from inside of the block in this case, just like the pre-existing standard block syntaxes.
It's up to the author of the code to create readable constructions that aren't misleading or confusing. There's no reason why this tool is innately bad, more tools can only make it more likely that the author will find an expressive and clear way to say exactly what they're doing without writing something confusing or redundant.
From a language purist perspective, these are all very good reasons not to use Ruby at all. I don't think that adding numbered positional params makes it worse in any objective sense.
Likewise
[[1,2,3]].collect{|x,y| [x,y]}
is perfectly valid even though 3 will not get assigned to anything.
So you're being forced to spell out which arguments you want to be able to reference, which is probably related to what it will get passed, but not required to be.
So if you don't know how many parameters there will be, you're in trouble either way.
Those can be perfectly fine names!
xs indicates you are writing a generic method or function applicable to just about any kind of collection, and x is an item in xs. So for functions like map and reduce or anything else that deals with arbitrary collections, these names are very clear in expressing the intent and far better than @1, etc.
This is similar to using "i" as the index variable in a for loop, an almost universally recognized convention.
If you're arguing that "fst" and "snd" are better names than "@1" and "@2" then I think we have a disagreement. None of those are good names. (All of those might be perfectly "sufficient" names, though. For a local variable, I would argue that spending any time at all on naming is a mistake. Spend that time writing a descriptive method name instead.)
https://martinfowler.com/bliki/TwoHardThings.html
https://hilton.org.uk/blog/why-naming-things-is-hard
https://www.quora.com/Why-is-naming-things-hard-in-computer-...
This is foundations of computer science stuff. If you choose a solidly descriptive but wrong name before the design is fully solidified, you may be stuck with it because it will work its way through dependencies. Even if you can rename it later during refactoring, the name will shape the formulation of those refactors.
I think all of this is overblown when it comes to talking about local variables in a block scope. This is absolutely a case study in the "bikeshed problem" at this point in the thread.
There are other features planned in Ruby 2.7 (like the :. operator) that actually make the operation of some code less discoverable and will hurt to see in your codebase for the first time, but we're not talking about them at all, because they're simply not as easy for us to hate on.
To say naming things is hard is like saying that getting a golf ball into a hole is hard; It is not. Sure it is if you're using some artificial restriction that you have to whack it with a stick from 400 yards away. Naming your children might be hard if you're trying to solve world poverty via the child-naming process. And naming things might be hard for programmers if they are not allowed to rename things or document the meaning and history of their names. But that really doesn't have much to do with language design.
>If you're arguing that "fst" and "snd" are better names than "@1" and "@2" then I think we have a disagreement.
I'm not arguing that. I'm arguing that they are not worse names. And as you probably recognized, if names like (@1, @2) are a good enough solution, then certainly (a, b), or (fst, snd) have an equivalently high bar for rejection.
One man's artificial restriction is another man's invariant. "We can't rename this thing without renaming it everywhere" is a thing, but honestly not in this case, because we're talking about a feature that is used to decide the name of local block variables.
The only place that local block parameter variable names will get used, except directly inside of the local block, is in blocks that are nested even deeper. We're not designing an interface inside of local blocks.
If you've read the thread and Matz opinion on the matter, the issue is that it's a requested feature and someone has taken the time to write it into the language. You need a good reason now, if you want to tell that person they can't have what they said they need. It was designed carefully so that it would not break existing valid uses of the language.
Maybe the block was meant to take 99 parameters, should the author be forced to come up with 99 one-letter variable names and type them all correctly, twice? The point is to provide another option, you won't be forced to use it. (I'm sure that Rubocop will even provide an option to enforce that your style guide specifies your devs must not use it.)
At any rate it sounds like this feature will land in Ruby 2.7 whether we like it or not.
They are better than fst and snd in a new iteration of Ruby, because giving a special meaning to existing legal identifiers in certain contexts risks breaking existing code; @1 and @2 (despite the similarity to instance variable notation) are not legal identifiers in existing Ruby so their special role has no effect on existing code.
That's just what scoped variables are. I've never seen it be a hazard other than shadowing names, and you'll have the same problem with @1, @2, etc. A closure returning a closure for instance.
Unless you're just completely misunderstanding me, thinking I'd be a proponent of fst, snd as direct substitutions for @1, @2? That would be absurd. I'm comparing |fst,snd| fst+snd to @1+@2. Code isn't going to break with either.