Things to avoid in Ruby
blog.appsignal.com
blog.appsignal.com
Sometimes I encounter code and go "I think I know who wrote this", and I don't have any problem with that. That's fine. People should have some freedom in the way they write code. A lot of the things I've learned in programming came from saying "ugh, I don't like this", and then later going "oh, that actually makes a lot of sense, and I'm going to adopt it".
I doubt very much there is any real developer time savings attached to enforced subjective uniformity, and in the case of rubocop I would say that the amount of time I've seen people changing this and that to satisfy the linter when what they wrote read just fine, it's probably the opposite.
If you want to eliminate most discussions about code formatting and have a useful subset, use standardrb[1]. I disagree with about a third of its recommendations, but I stopped configuring with standard, whereas I had an override list at least thirty rules long (that had to be updated periodically because of incessant patch version renaming of rules) with vanilla Rubocop.
Rubocop's engine is good. Rubocop's default rules are best avoided and replaced entirely with standard, because they are at least mostly sensible and still allow for "style".
You can also have different rules for different folders or files, and toggle rules on or off by line, block, or files.
The part about people spending significant time fixing linter errors sounds suspicious though. That doesn’t seem normal at all. Have they heard of format-on-save, or is there a lack of communication?
I really believe it's the least bad solution to just get any config and ignore the bikesheding (or drop people who spend too much time on it)
Very sad.
It does sometimes recommend things that I don’t agree with sometimes, but in exchange I don’t have discussions about style on a PR, which makes it worth it. That’s objectively less time spent talking about these things.
Then on top of that there are some rubocop extensions, the rspec one especially, that forces you to write bad, unreadable tests with the worst performance footprints. So unless you are in a company with a good culture where that talk does happen, I would keep rubocop for security sections.
In rspec it forces everything in a let, with single expect per test, so you can never check if two things are set to something at the same time
Elsif? How about a case statement?
A lot of the idioms are great until you need to debug them. At that point you have to replace them with non-idioms because you can't even debug the code you wrote without changing it. Why give someone grief for starting with something debuggable?
I dislike the social pressure and "Rubocop's Karen-ing" used to inflict these idioms when they are a mixed bag, at best, and demonstrably worse in cases.
I think this blog is called 'Ruby Magic', and Rails especially has that reputation of being inscrutable magic; these suggestions just make it worse. Forget idioms and using obscure features of the language or framework - just write simple clear code.
So if you had a model Activity, and you wanted to get the names of some of them, the slower/higher memory approach would be something like:
Activity.where("id < 100").map(&:name)
This pulls results from the database, instantiates each as Activity objects, then iterates through each to get its name. You can see in the logs that it grabs the entire object:
Activity Load (133.7ms) SELECT "activities".* FROM "activities" WHERE (id < 100)
.pluck instead grabs just the field you're looking for without instantiating the entire object. So the better way to grab the list of activity names would be like:
Activity.where("id < 100").pluck(:name)
And you can see from the logs that it makes a more narrow query:
(3.8ms) SELECT "activities"."name" FROM "activities" WHERE (id < 100)
The whole stack is a dumpster fire filled with footguns.
Where “show me where this function is defined” turns into an impossible task.
Overuse of dynamically generated functions is possible in any high level language. The worst offender that I had seen was a PHP wordpress plugin that stored a custom function per row in the DB brought to life via `eval`.
Those were the days.
A large number of high-level languages resolve functions at compile time, rejecting programs containing references to functions that don't exist.
I will agree that dynamically generated functions are not overused in languages in which they cannot be expressed at all, but...
It's a terrible experience. No auto-generated specs. Can't even look at the code to know what some request parameters are supposed to be (had to go digging through pagy docs for that one). One day, a value in a response that is normally a string was suddenly an array on certain queries.
The whole approach is so haphazard move-fast-nobody-cares that it grinds my gears and I'm not even contributing to their code, just consuming it.
My kingdom for a better experience.
On top of that, the number of bugs that could have been avoided with static types is a bit depressing- the only true way to avoid them entirely is duplicating what you get from a static language, but manually with extra unit tests.
There's plenty of really nice things about Ruby and rails, but you don't actually get features faster without sacrificing something.
The team puts out code at more or less the same pace as I would have expected from teams working with other languages, though I haven't seen so many nil errors since working with a bad Java code base.
Second, what happens when the code is in a different repo and there are zero imports so you have no clue where it’s getting loaded from?
We’ve got multiple gems and microservices so I can’t even grep one codebase, I basically have to grep our entire gh org to find the function because ruby has terrible tooling to help with “go to definition”.
To even have a chance of debugging it we had to pull out more variables etc so we could breakpoint each part rather than it being this complicated one liner.
After they debugged it, and fixed it, I watched them rewrite it back into the complicated one liner because it looks nicer...
I just don't get it. It's like they just didn't see the pain they went through.
We built our own hells.
A critique: 1. Verbosity vs Idiomatic, to me I find there are times the verbose approach is actually easier to read and understand
2. Long Expressions to Detect nil - this seems a bit like a strawman, sure there are better ways than to check the presence of an object/method chain, but doing the safe operator can bite you in the leg by compacting the complexity
3. Overuse of self - I skimmed through the Rectangle class declaration... it reminds me of writing Java. I rarely see self#method= used
4. Collecting Results in Temporary Variables - I think this is debatable, there's value in condensing method implementations when it makes sense, but also having temporary variable assignment can lead to better readability
Regarding (3), I agree, after seven years on a massive Rails project working with tons of very talented Ruby developers, self#method= is typically avoided. However self#method is usually preferable -- when there's no assignment prior to reading the name, it's highly clear that the name is a method. There's always exceptions though.
Re (4), this is true but the decision of which calculation merits a temporary variable (assuming it's not needed for multiple use) can be pretty arbitrary. Elsewhere in this thread there is the sentiment that extra temporary variables increases debuggability which perplexes me, since most commonly Ruby debugging is done in a command line debugger like pry/byebug, and in that context you can simply evaluate part of the "packed" expression as needed. In languages which primarily use graphical debuggers and perhaps do not have the ability to evaluate arbitrary expressions, this is much more of a factor.
2. About Long Expressions to Detect nil: rails devs have had Object#try fir many ears to avoid that, and &. syntax since ruby 2.3 (10 years?).
3. About Overuse of self, again, clearly the author hasn't done enough python... and that's hardly common, given the many options ruby gives you (ivar access, self-less implicit method calls...)
4. About Collecting Results in Temporary Variables, ruby has supported map/filter/reduce for more than 15 years. This was however quite common code to find years ago from ex-java devs...
5. About Sorting and Filtering in Memory, that's actually the only legitimate problem I can think of. However, I'd hardly consider that Ruby's fault, and more of a superpower of composition of abstractions (datasets to enumerables) which should obviously be handled with care (and perhaps linked against? Does ruboxop support smth of the kind?)
Actual thing I see many times and should be avoided: construction an hash of options first, then pass it as kwargs:
opts = {
foo: 1,
bar: 2
}
meth(opts)But what's wrong with an instantiated hash as kwargs? :)
meth(foo: 1, bar: 2)
# even if multiline
meth(
foo: 1,
bar: 2
)
while needlessly allocating an hash (kwargs optimize that).But I like to use a prepared hash when there's preparation that must be done, or if it just makes the data more clear.
The recommended fix here is…needlessly verbose. Better would be:
actor =
case response
when 1
"Groucho"
when 2
"Chico"
when 3
"Harpo"
else "Zeppo"
end
or even: actors = {1=>"Groucho", 2=>"Chico", 3=>"Harpo"}
actors.default = "Zeppo"
actor = actors[response]
2. Safe navigation operatorYes, use this for sure, this is good.
3. "Unnecessary" use of self
Use of self to refer to instance methods (the text here refers to "model attributes", but that's just a particular use of instance methods) is appropriate and often desirable, because failure to do so as the example demonstrates earlier can result in bugs if the code is ever changed to introduce a local variable of the same names as the instance method. Now, sure, generally I'd prefer to avoid doing that, and in a very short method (especially a one-line defining what is essentially a getter for a derived trait), sure, don't bother, its cleaner and easier to comprehend without additional noise and no one is going to change it to add a local variable without seeing the other use and fixing it. But, in general, self is clear and safer.
4. Collecting results in temp vars
Yes, avoid this by knowing and using the methods on Enumerable that are designed for this, as they clearly and concisely express what you want to do.
But also, remember the first rule about Verbosity, so avoid stuttering and superfluous parens.
h.transform_values {_1 &. *2}This was my rationale originally, but after working on a big Rails codebase for so long, and hearing a lot of push for the opposite conclusion from Rails devs more experienced than I am, I have relaxed my opinion here. It is certainly true that always using self will avoid the type of bugs you speak of, but if you also aim for short declarative methods over long imperative ones (as you mentioned), this issue becomes pretty rare, and in a language with so many other ways to shoot yourself in the foot, really the only good way to insulate yourself against bugs is with automated tests.
What you gain from not using self is more compact expressions that are less likely to bleed past the line length limits or need to be split up into temporary variables, which does help to look at the big picture of an operation with less syntactic load on the reader of the code.
All that said, if you have junior Rails devs on your team, it might make sense to tailor your style closer to overuse of self to ensure they can more easily grok it.
I'm also a huge fan of using early return to handle edge cases and exceptional cases. This is something a lot of developers disagree with me on, but I've never been sufficiently convinced to avoid it.
this needs a big fat asterisk.
sometimes sorting in the database requires a filesort
if you know ahead of time roughly how many results are getting sorted in-memory it absolutely can be faster to do it in-memory than ask the database to do it
However, the amount of times you want to order a subset of records independent of the complete set is quite rare, as almost always you are doing some kind of paged fetch, and it makes no sense to use an indeterminate database ordering, even if you are just ordering by primary key (which is always indexed in ActiveRecord)
His 'more idiomatic' code is both harder to read (arguably) and harder to debug. In his "bad" example, it is very easy to set a breakpoint at each important step of the function, or insert logging, or whatever. The enumerable approach yields fewer lines but it's really difficult to stop the code and examine where something went wrong. Not to mention it's a bunch of symbol soup gobbledygook that takes a lot more specific Ruby knowledge to decipher.
Personally I'm all for temp vars. When I have to come back to the code in 18 months or whatever I will thank myself for making it so easy on me.
Having variable assignment after assignment after assingment is also an indication for a function potentially doing too many things and might even be called a code smell.
Defining variables at the bottom of the class, WTF?
Ruby produced some of the best light-config or code-as-config software I've ever seen, and thankfully forced many aspects of Java's XML-SOAP-boilerplate problems to be finally addressed and thrown out.
But Groovy has static types and a legit JIT on the JVM with @CompileStatic. I agree very weakly with a lot of the contentions in the article because they are also major language advantages Groovy has (or had) over straight Java, but it is deck chair rearranging.
Ruby has had problems with performance and the usual problems with type-light scripting in large codebases. I don't know how good Ruby IDEs are, but fundamentally it has to be challenging to know what type and methods can be called on something being returned from a ruby expression, although it is amazing what Javascript IDEs can do with type inferencing so what do I know.
Groovy is a dead language though, killed by Kotlin, and Ruby is getting destroyed by Python.
I prefer to avoid Rails (it’s not bad, but it’s overkill for most things I need). The Elixir language has some superficial resemblance to Ruby which helped with language acquisition 8 years ago or so, but it is very much its own thing. The resemblance is unsurprising given José Valim's background with Ruby and Rails itself.
Most of my quick scripts end up being Ruby, but I prefer to think in Elixir (mostly for pattern matching) or Typescript (for structural typing). The libraries I maintain in the Ruby community support much older EOL versions of Ruby, so I haven't been able to adopt new syntax like pattern matching for much.
As a language it is completely unsuitable for large projects, the dynamic features that are helpful at a small scale become liabilities at a large scale.
Ruby’s main flaw is the excessive exuberance of its fans, you wouldn’t develop a large website in bash, you probably shouldn’t do it in Ruby either, but each of the languages still has its own charm and is useful for the things it was designed for.
Not sure where you are getting this.
This sort of "language/tool/framework is a dumpster fire" post is not helpful or curious. Every tool has its flaws and places where it excels. Painting with broad strokes ignores nuance and implies a lack of familiarity with why the tool gets used.
Besides, PHP exists (/s)