"Useless Ruby sugar": Argument forwarding
zverok.space
zverok.space
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.
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.
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=2It’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++
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(...)Expect to be building your own environments if you want to run things that are not in your distro.
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.
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.
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...
No. Give the same environment as production.
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).
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
endBut I prefer to replace ‘raise NotImplementedError’ with ´raise “Not Implemeneted”’ because that exception has different semantics (method unavailability on the current platform)
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")
stupid formatting
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
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.
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".
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?
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.
(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)
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.
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.
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.
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.
Why so?
Keyword arguments remove the depedency on the position.
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.*
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).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.
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.
It’s more the opposite, arrow functions treat `this` as a regular lexical variable, it’s normal functions which special-case `this`.
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.
Essentially, it offers a server-side alternative to "prop drilling," where you repetitively pass the same parameter through a chain of function calls.
I prefer implicit variables like scala has but it gets the job done in C#.
I also write my Ruby Seattle style, so I’m one of those weirdos.
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?
“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.
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 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.
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.
(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.We need a standard for this. When I'm writing example code, or redacting real code, I usually opt for an ellipses with spaces: `. . .`
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.
(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.
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.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.
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. $ .(){ "$@"; } # define function called "."
$ . . . # nothing
$ . echo hello
hello
$ .(){ :; }
This creates a function called "." that does nothing.Then this is valid code:
. . . def `. . .` = println("That's valid syntax!")
@main def entryPoint = `. . .`
[ https://scastie.scala-lang.org/BsvbSctXTmaGWaCbM6JX9Q ]I said almost because HN suppresses the back-ticks…
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.
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.
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.
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.
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.
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.
Pedantic tangent, but `includes` is actually the singular form: "This array includes that object" vs "All of these arrays include that object".
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.
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.
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.
"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.
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?
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.
The major piece of the puzzle missing is generics support, but other type handling is already there.
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.
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.
>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.
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.
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.
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.
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.