I Love Ruby
eliseshaffer.com
eliseshaffer.com
The supports_feature-method is probably defined somewhere 5 abstractions deep. If you are lucky that is, it might also be part of some library's weird meta-programming of supports-* that no LSP can point you towards. I've never worked in an ecosystem that celebrates implicitness as much as ruby does, and it is driving me nuts.
The fact that finished code looks great and reads well doesn't balance the scales in my book.
Even tough you can reach for metaprogramming (like define_method or method_missing), that's really not how the entire ecosystem of guides and tutorials will point you.
Anyways, when in doubt, just plug a debugger and call "@subscription.method(:supports_feature?).source_location`, and generally that's all it takes.
As for the abstraction, Rails does take it pretty far, but you also have to take into account that it's a framework that supports numerous pluggable strategies and disparate backends, and extensibility. I think most mature frameworks are pretty complex-- the goal of the framework is that it's all encapsulated away from you for the vast majority of tasks, and in Rails, that is extremely true.
This is the classic mistake of confusing Ruby with Rails. That might be true of Rails. It absolutely isn't true of vanilla Ruby. The article was not "I love Rails".
Rack or Minitest for example are very Ruby. RSpec on the other hand is full of Railsy magic, just yesterday testing some threaded stuff I was bit by `let` because it has concurrency consequences, and the magic becomes darkness as you have to look behind the curtain and beat it into submission in roundabout ways that obscure the thing you're actually doing.
† or sometimes is a minefield, all by itself, like this nonsense: https://github.com/rails/rails/blob/1512cf2ba578282c898b8eb6...
Reminds me of Angular vs React-- In my opinion it's much easier to get immediately productive in React, but it's very easy to produce a rats nest over time if you aren't experienced with the framework. Versus Angular which has a lot of upfront learning curve, but it's prescribed structure makes it fairly easy to scale out the app over time.
Rails is definitely more like React in this respect. It turns out you can absolutely color outside the lines and still enjoy the speediness, but it's not something that I experienced devs should try to do without making a huge mess.
Ruby is not Rails, but Rails is Ruby.
edit- as another comment points out, you can just do `@subscription.method(:supports_feature?).source_location` too
IMO this is because software engineers both get bored and feel a need to compete. The more abstract they can make code the better (more safe?) they feel. That lets them play chess on their terms and eliminate the competition. But that's just my take, I may be wrong. Fortunately the most commonly used gems wrap this abstraction into usable APIs, and most have good documentation.
While this can be correctly attributed to ruby or the devs in its ecosystem, I've been on teams with staff using other languages and stacks that let their completion/anxiety fly making it hard to read and understand their code. I guess my point is it's a good critique, but it falls beyond the meta programming in ruby.
Yes, but pro tip: you can do object.method(:supports_feature).source_location to inspect where it comes from. It may be a module included into the class, the class itself or one of its super classes, or a concept built on modules like ActiveSupport "concerns". But source_location works in almost all cases.
> might also be part of some library's weird meta-programming of supports-* that no LSP can point you towards
Yes, if this comes from method_missing, you can't check it's source_location. You need to use your knowledge that the method name works and that method_missing is how such things are done, and then you do object.method(:method_missing).source_location and read the logic there.
The lack of safe and reliable IDE introspection and design-time type support are my number one issue with Ruby, and why I tend to use typed languages instead.
However, there are workflows for inspecting a codebase that work, they just don't fit in well to the usual LSP+IDE pattern. You are expected to use irb/rails console to do your inspection of the structure and behavior of your programs.
My Ruby projects often have a wrapper that reloads code and puts me in a pry prompt. I can edit code and introspect objects live as the code changes.
It's a natural extension of Ruby's Smalltalk legacy.
I recently replaced my X window manager with one I've written in Ruby, and I can just attach to it and modify state or change code live to test or fix things.
Being confined to JS and Java in VS Code for work is like having one arm tied behind my back.
That sounds very interesting! Can you please share the wrapper that does all that magic? Thanks!
def reload
load "app.rb"
# If you want to load multiple files, remember that if app.rb
# uses require/require relative, the files will only be loaded
# once; you can change that but really it's not usually what
# you want for production; in that case either look into a proper
# reloader, or you'll want to load the rest of the files here.
# Sometimes it's fine to e.g. do a Dir.glob("*.rb").each {load _1}.
load "dev.rb"
# Put "dev niceties" in this file. E.g. maybe load table_print or
# awesome_print, Hirb, or whichever nice formatting tools you
# want, or define any custom introspection methods that are
# helpful. Eg. for an analytics project I worked on had a few tools to
# wrangle CSV test data there. (I really ought to extract the best
# from my various projects into a gist or something, but these
# files often accumulate really project-specific stuff.
end
require 'pry'
reload
binding.pry # Throws you in the Pry prompt at the top level scope.
It does require that you're somewhat careful not to make the loading of your files too stateful, which is a good practice anyway, and you do need to be mindful that things will occasionally fall apart if you reload the running code as it will modify the classes of objects that already exists but not e.g. update their instance variables, so if you add a method that expects @foo to have been initialized, but existing objects do not have it initialized, things will go obviously go badly.Since pry supports "edit some_method_name" and will spawn $EDITOR you can even do edits in the same terminal that way, but I tend to prefer to have my editor open in another window. (And since so many here seems to struggle to find methods, in addition to "edit", "show-method some_method" in the right context in pry is also highly useful)
Sometimes I'll keep ways to trigger pry in applications during regular runtime because it's so useful for debugging issues, and sometimes even fixing issues in a running process.
EDIT: While I prefer pry, it's worth noting that Irb has gotten a lot better (lots of features from pry) in recent versions, and also "rdbg" (debug gem) is awesome if you don't want to use a separate script like this - you can equally well run your code under rdbg and just have a script handy to load tools you want for a debug session; the downside of rdbg is if e.g. attaching remotely you'll be running in a trap context, which means you don't have quite the same freedom with respect to what code you can actually run - that may or may not matter.
Just let the bliss wash over you and ignore the haters.
But I don't let it bother me. I instead enjoy rewriting an ever-increasing part of the software I use day to day in Ruby (my editor, shell, file-manager, contextual menus, menu bar, font renderer, window manager so far - I keep telling myself I need to stop myself before I start writing an X or Wayland server too, or even worse, before insanity takes me and I start writing a browser)
I think the real starting point for me is going to be to clean up PureX11 a bit more so the API is at least somewhat cohesive, and then push the WM as it's working enough that it's been my only wm for a few weeks (it does have significant quirks still, but with somewhat minor cleanups it's a decent starting point to play with), and then the terminal as it's fairly freestanding, then some of the file management tools, toolbar, popup menu etc., then lastly my editor. The editor has by far changed most from the version on Github and is also most likely to cause problems for others, so that might take a bit of time, not least because I'm in the middle of a fairly significant overhaul of the way the views and models works.
Here's some of what is out there, though:
* Skrift: This is a Ruby port of libschrift, a TTF font renderer. It's heavily cut down, and currently stands at about 680 lines of code. I intended to tidy up the API as it's still a bit messy after my rewrite: https://github.com/vidarh/skrift
* X11 bindings for Skrift: https://github.com/vidarh/skrift-x11 - these are messy, and I have significant updates to them (including basic fontset support and a mechanism for pixel-perfect boxdrawing characters at any reasonable scale) that have not yet been pushed: https://github.com/vidarh/skrift-x11
* Pure-X11: This is a form and significant overhaul of pure X11 client bindings for Ruby (as in not Xlib or XCB needed): https://github.com/vidarh/ruby-x11 - it's not terribly out of date, but it's a bit in flux as I don't like the initial mechanism, used for the protocol and so I'm thinking about how to trim it down and make it easier to use.
* This is the starting point for my terminal. My terminal is nothing like that any more, but this is the repo that will get all the updates, eventually: https://github.com/vidarh/rubyterm - this initial prototype used a C extension and server-side fonts, while the current version uses Pure-X11 and Skrift
* This was the very first version of my WM I used, a few hours into the switch (from bspwm). It's a straight port from TinyWM. My current one has tiling and some EWMH support and multiple desktops and adds about 700 lines of code - it'll start appearing on Github soon: https://gist.github.com/vidarh/1cdbfcdf3cfd8d25a247243963e55...
* This is a script I used to feed into a 9menu style popup menu script from my file manager to generate folder-contextual actions based on the folder contents: https://gist.github.com/vidarh/323204137de5293bfe216ec751646... -- the current version is quite a bit slicker and will eventually show up
* This is a very dated and broken version of my editor, and odds are you'll struggle to get it to work at all, as it depends on various helper scripts that are not yet packaged up, as have been massive updated since that version; I'm hoping to maybe bring the repo a bit more up to date over the holidays: https://github.com/vidarh/re
* This is a gem that handles the input processing: https://github.com/vidarh/termcontroller
* This does keyboard mapping from symbols from termcontroller to higher level user-defined sequences: https://github.com/vidarh/keyboard_map
* This is layered on top of Rouge (which I use for syntax highlighting), to load GTK sourceview themes into Rouge: https://github.com/vidarh/rouge-gtk_theme_loader
My editor is in Ruby, and uses DrB to talk to the backend. It has a key combination to throw me into a pry prompt, and also throws me into a pry prompt if any exception is thrown. Since DrB will forward any exceptions during the execution of any messages (and here we really see the message passing bit - they are literally messages passed over a socket and via proxies that don't know what they mean) back to the client, this means that any exceptions in the server will throw me into a pry prompt where I can dynamically edit the code of the server process and continue execution.
The server process holds the open buffers, and I can reattach to it, and thanks to that use of DrB I was able to switch to using my own editor day to day at a point where it was still wildly unstable without losing any data. Sometimes I'd edit the editor with itself while it was in a broken state, and reload the offending code to fix things and just keep working.
With most other languages - with a few exceptions - I'd have to wait until I had something more finished and more stable to start using things. With Ruby I often feel comfortable with starting to use projects while they're still crash-prone and half-finished and lacking because it's so easy to metaphorically replace the engine in mid-flight.
The beauty of the DrB part of it, though, is that the shell of that is very small: Just spawn a DrB server, spawn a client, and wrap the client in a begin/rescue block with binding.pry, and put any critical data on the server side. Suddenly your server-side is near crash-proof and your data much less likely to disappear. If you want an extra level of protection, run a threat on the server side to checkpoint the data regularly (I used to checkpoint it every 5 seconds at the start; it's now at around 5 minutes, which also means every buffer I've opened and not bothered to kill from the last 5-6 years are still accessible... I never bothered to add code to clean them up as they just don't take up much space)
Occasionally I may restart, if e.g. object state has changed enough, but this means that if I've set up a bunch of test data for example, and run into a bug, I can fix the bug, "reload", and the state of the running objects remain the same but the method will have updated and I can retry the same method call with the exact same object state.
This is true in the dev space too. In dev, you can just set breakpoints by inserting a pry/byebug call and the debugger is just part of the application at runtime. Can you do it in a sufficient Ruby IDE too? Of course. In my experience this isn't super common though, and isn't going to give you the best debugging experience, because it's separating you from that hands on direct inspection context.
And IDEs like Rubymine like to think that the excellent deterministic automatic refactorings of languages like Java are actually feasible in a Ruby codebase, but I have seen that first hand go horribly wrong with a developer who trusted them.
Even within the Ruby community a whole lot of people just stop at fairly basic exploration in the Rails console and don't explore the full level of flexibility they get you.
And I think a good Ruby IDE would be one that tried to be more of a Smalltalk like environment - if you want to refactor Ruby code, you're far better off starting by introspecting an actually loaded app environment rather than starting from a static parse.
Yes, but this is something that IMO didn't age too well when we transitioned from pet servers to cattle servers (and that's even more true in a serverless scenario or even just Kubernetes). I'm not saying that you can't do it but 1) it will discouraged by security best-practices in most places 2) the Ruby in which you are in might disappear under your feet in a second
A significant part of the utility is in development, and that doesn't go away.
> I'm not saying that you can't do it but 1) it will discouraged by security best-practices in most places
I've yet to see an ops team (I've been involved in ops work for 28 years at this point) that doesn't have plenty of escape hatches, irrespective of the language. One can pretend that it's locked down all one wants, but the moment the ability to open a root prompt in a container exists that is just an illusion.
The most brutally locked down systems I've seen still wouldn't usually prevent you from getting access one way or another (sometimes the means of getting access have been painful, like going via an IPMI console or other "fun" detours, on a very few rare instances it's required physical presence in a specific location; most places the most locked down setups you'll tend to find require a VPN or going via a bastion host), but would ensure the containers were destroyed after you exit (to prevent cattle from turning into pets... so you can debug but not leave changed state).
I'm not saying there aren't people who actually lock their systems down to a point where their ops team can't gain access to use a console; but I am saying that even a lot of places where people think that is the case, it often isn't actually the case. It may well be locked down too much for developers to be able to use it to debug in normal circumstances, though. I'd rather people are honest about it and secure and restrict the access properly than pretend there aren't workarounds that are in regular use, as I've seen way too many times.
> 2) the Ruby in which you are in might disappear under your feet in a second
If you don't have a way of mostly preventing processes from being killed when a connection is ongoing, that is a choice you made. If your technology is fighting you because of choices you made and you keep it that way, that is also a choice. Maybe it's right for you, but no system I've built would randomly pull the rug on me other than due to actual system failures.
Yup, one day someone was poking around our mess of over-engineered k8s web app ephemera, clicked a button, and realized we finally had a working Rails production console, albeit one in a web browser, without our tooling, and that would drop connectivity every 15 minutes. When this was discovered this was told to be all part of the plan… welcome to the spin zone.
Which was awesome and super efficient and kept developers close to the production environment and the necessary knowledge of that context to properly build applications.
Then DevOps came along with a bunch of great new ideas…………
The behaviour of a Ruby program, right down to the types and methods available, is only defined by running the program itself. It allows for all the glorious freedom of meta programming for which the language became famous as well as the often times inscrutability of the source code.
With a language like Python, the source code at the start bears a good resemblance to the structure of the program when any particular function executes.
With Ruby, the source code at the start of a program is merely a hint as to what the ObjectSpace will contain when your particular function ends up being called!
>>> NewClass = type('NewClass', (), {'foo':42})
>>> x = NewClass()
>>> x.foo
42There is no proper tutorial, only links to external resources of varying accuracy and levels of update-ness.
It really doesn’t help with grasping such a customisable runtime.
I think an overlooked part of my point in this post is that the language felt intuitive to me from the beginning. It really matches the way my brain was already thinking about code. Think in messages, focus on small objects and what messages they can receive. There's a lot of comments about not understanding Ruby or finding it hard to maintain, but we're all different. I know people who feel the same way I do, but about Javascript, or Kotlin, or even Objective-C(which I find impossible to follow).
I get that Ruby isn't everyone's cup of tea, but for me it's pure joy. And when I'm working with other Rubyists, it feels like we're flying along effortlessly.
I wasn't really trying to comment on other languages. And everything we choose is based on our own design sensibilities, our constraints, and our experience. I really just wanted to put down in writing, what sparks joy for me about Ruby.
When I first took on a project that was written in Ruby I was extremely annoyed. Once I learned my way around and really understood it better, I loved it. It's still my go-to for just about everything just because of how productive and readable it is.
The performance optimizer in me fights a constant inner battle about the fastest way to do things vs the most productive way to do them. With more time and experience, productivity wins just about every time.
I'm also a huge fan of Elixir and it's always interesting to watch the language discussions because there are people who are absolute "static typing is the only way" zealots out there. I don't use zealots lightly either. Preference is one thing. "Any other way is wrong" is entirely another. Elixir gives you a different way of achieving the same thing with strong typing, pattern matching, guard clauses and type inference via symbols...but it's still not enough for the static typing crowd, no matter how perfect the balance of concerns.
When you find your language, enjoy it. There's always going to be people who find a reason to dislike your choice because it wasn't their choice.
I'd love to see some improved typing support in Ruby, but it's gone from a "the world will fall without it" to something that'd be a minor nice to have and which doesn't need to be complete.
I've moved on to other languages now, but I treasure the time I spent doing Ruby, and I've carried many valuable things onwards with me.
Things might change for you too, over time, but that won't lessen the positive impact that Ruby has had being so close to the way you naturally think about programming.
Your blog post made me really consider giving Ruby another shot, and I’m curious if you have any good places to start for someone familiar with programming but not Ruby.
Thank you, and I’ll definitely be reading more of your blog :)
arr
.map{|o| ... }
.reject{|o| ...}
.reduce(init_acculm){|init_acculm, o| ...}
are super super clean and expressive. Very similar to what I like about Java streams.And the library ecosystem is great, I like how it shares spiritual similarities with python where libraries are very "no nonsense" (you don't need extensive configuration and builders and researching a million configuration items etc etc... looking at you, Java) and you typically just import and go. Rails, of course.
I keep picking python over ruby though, for things I'm going to have to actually maintain. And I typically pick Java over python if I smell I'm going to care even an iota about performance. (Often I don't, though). But ruby vs python, I keep coming back to the divergent opinions they've taken on gradual typing. I like that python3 lets you include the types as part of the program, part of the grammar. Ruby relegates them to a separate file. I guess the intent is that it's more for libraries, like how js libs will ship typescript type files? But I don't like that, I want types for myself. Sorbet exists, of course, but I don't like that it's a) a gem and b) still not a first-class part of the grammar but is instead just operating "in-language". I know it works and e.g. Stripe uses it to great effect (I worked there) but I just don't like it, personally, and I find that python3 with its built-in type hinting tends to get typed more readily than ruby where it's a much further reach away.
But I really love ruby. I hope it improves its type-hinting story because I like most other things about it. But I was pretty unenthusiastic about python prior to it getting its type hinting built-in, so apparently this is a big deal for me.
Python is a lovely language, but context managers often feel like a shabby substitute for what I can do with `yield` in Ruby :-)
As for functions, Ruby doesn’t really have them—or, rather, blocks and Procs really are the closest you get to a function. Instead, since everything is an object, Ruby has methods instead of functions. Critically, because methods are defined on objects they have inherent object context, which is why you can’t treat a defined method as an anonymous function. Of course, there are ways to extract the method as a Proc and pass it to another object (with or without its object context). But having methods be objects the way they are in, for example, JavaScript would require extra magic under the hood, and lead to all sorts of context gotchas, which is probably why it doesn’t work that way.
If you’re prototyping a new interpreted language and don’t want to have to build a JIT that can optimize code, but still want some semblance of good performance, the block approach Ruby picks can be pretty useful.
Ruby doesn’t haven to allocate a full-on, garbage-collected, closure object every time a function accepts a block: it only has to do this if the block gets stored to a variable. If the block is only ever yield’d to, the allocation can be skipped.
And when your language’s primary looping mechanism is done with blocks, the difference adds up:
xs.each do |ys|
# with normal closures and no fancy JIT,
# the VM has to allocate a closure once per loop:
ys.each do |y|
end
end
Ruby was able to get away with its closure-heavy standard library APIs without a JIT for almost 3 decades because of the affordances that blocks provide over procs/lambdas.Ruby doesn't have functions at all - it has objects with methods, and you can treat a block as an object whenever you want.
IMO the ruby one is more powerful because it effectively allows you to customize language syntax by supplying a block. And it fits nicely with the stdlib and libraries (ex map yields to a block to do the mapping). Ruby also has an enumerator that does some similar things to pythons yield: https://docs.ruby-lang.org/en/master/Enumerator.html
Pythons yield is more commonly used for iteration and stuff although it does allow some neat tricks for scoping things.
More generally, and more to your point, I think extensive use of blocks/HOFs lends to the "spirit" of Ruby being a lot more functional-style than python, which iirc got lambda support late and they're kind of ugly (I mean, you literally type lambda! almost as bad as go!).
The problem is that Python type hints are not enforced at runtime. They can can not be trusted. You have to rely on external type checkers that are relatively slow and different checker might give you different results.
Probably the only mainstream backend-language getting gradual typing right is PHP. Yes, good old PHP. Dead simple type system, no productivity-sucking compile step like in Typescript, actual runtime enforcement of types contrary to Python and great static linting.
Though PHP is also a fair bit less expressive and dynamic compared to both Python and Ruby so it can get away with an much simpler type system. Then again, as someone who has to work with many legacy systems, sometimes I am very grateful for the lower expressiveness.
Sorbet has a runtime time-checking mode I think, too, so that's actually a point in favor to ruby for you (though again with the drawbacks I mentioned of being a gem and imo being a little clunky syntax-wise).
Whether you agree with it or not Ruby's (the language not the developers) resistance to static typing is just how Ruby wants to be written. There are no types, only messages. The whole culture surrounding Ruby is burying all the underlying data in favor of pure message-passing nirvana. I have no ducks but I must quack.
Yes but...what is the shape of those messages? I ways prefer to know the type of the material I'm working with...
I've also never written anything particularly large in it. Like around a few thousand LOC, tops, for any one tool. And at that point, I often find myself writing a lot of things like assert { foo.is_a?(Foo) } to check parameter types, because beyond a certain size I just can't keep all the details in my head, and I don't want to rely only on comments.
Guess I'm saying that even though I really love it for small things, I'm also skeptical of its ability to scale.
I really like the way Swift introduced closures as regular parameters, but lets you have the Ruby-like “trailing closure syntax” for the last one in the list. It’s very best-of-both worldsy: if you need just one closure, it can be trailing, and if you need multiple, they can both look the same.
It's not necessarily the language itself, which is a reasonable language, it's the patterns that are common in the ecosystem:
1. Use of inheritance for code sharing. It makes it extremely hard to reason about any piece of code I am looking at. Where does the `param` variable come from? Is it injected by any ancestors of the class I'm looking at? Who knows, I have to use the debugger to find out. I cannot reason about the code without it. Of course you can use Ruby while preferring composition over inheritance, it's just rarely done by the community. Other modern languages like Go and Rust wisely leave out inheritance for code sharing in their object model and arguably have much more readable (albeit verbose) code.
2. Global mutable state is everywhere. Not sure if it's Rails architecture that encouraged using global state as request local state (until they found out that this makes concurrency hard), but Ruby codebases are full of global mutable state. It's everywhere and again makes it hard to reason about dependencies between objects and how they interact with each other. Again, this is nothing that the language forces people to do, it's conventions.
3. Overuse of meta programming. Ruby's metaprogramming is really well done in my opinion. The problem is that lots of people want to use it and do so in places where it doesn't provide enough value to justify the costs. It's an authentication library. It doesn't need it's own DSL.
Bad news buddy:
https://gobyexample.com/struct-embedding
At first I loved it, then quickly realized as you did that it makes code essentially unreadable.
And the embedded struct has to be part of the struct definition, it can’t be snuck in by reopening the class and include-ing new bits.
Ruby's first release was in 1995. Go in 2009.
As for disliking Rails, join the club, plenty of Ruby users dislike Rails too.
a. Yes, global mutable state is anti-functional style, side-effect-oriented, god function/object programming and it sucks.
b. The lack of an ability to seal objects, classes, and modules from further modification is a problem.
c. Ruby 3 should've broken compatibility by adding gradual typing.
d. Nonorthogonality - Too many ways to do the same thing.
e. Dying community - Too much broken code out there.
f. Ruby was "Perl5 2.0" in most senses.
For type-safer things that look like Ruby, Crystal is an almost viable, interesting alternative.
Otherwise, I have evolved past Ruby because of the learning curve for others, too many people hate it, too many maintainers have attitude/professionalism problems, and there are more supportable alternatives for most use-cases such as rust, elixir, and bash. Sometimes, it's still useful to use ruby for some quick text formatting or a quick CLI utility in the space of awk, perl, or sed. The world mostly uses js and python. One can either be a hipster with obscure tools or get things done with the lowest TCO/highest ROI.
"freeze" has been there from the very start". You can freeze objects, or individual classes and modules (because, hey, they're objects) just fine. I'm all for freezing more stuff, and the migration towards freezing string literals was good, and you can freeze very aggressively in your own code, including system classes.
> c. Ruby 3 should've broken compatibility by adding gradual typing.
If they had, it would've split the community. Some of us would've forked it. Any degree of gradual typing in Ruby will need to be extremely cautiously approached and/or mostly optional, as it strikes at the heart of what makes Ruby what it is. There are places it would work and be fine, and there are places where it would massively hamper productivity.
> For type-safer things that look like Ruby, Crystal is an almost viable, interesting alternative.
Crystal has a fraction of the community and too many pointless differences to Ruby for many of us to consider.
> The world mostly uses js and python. One can either be a hipster with obscure tools or get things done with the lowest TCO/highest ROI.
Funny. My career has spanned multiple "the world mostly uses $language" phases, and it's never affected my TCO/ROI whether what I've happened to use matches the current $language. What has mattered is picking a language the right libraries are available for and that I'm comfortable in. Sometimes I only get one of those, and sometimes that means picking a different language the Ruby, but writing composable tools means it's quite rare, and each time makes it rarer.
Here's a challenge: in the Gitlab source code, tell me where `external_file_project_job_artifacts_path` is defined. It's referenced in a few places but it isn't defined anywhere.
And yes I know in other languages you can use codegen to construct unsearchable identifiers in the same way, but Ruby seems to actively encourage it.
Those are some pretty clean routes!
Because of that a lot of people still have major bad practices set in their heads such as a method shouldn't be longer than 5 lines etc.
Most rails code bases that I have seen usually has had a senior ruby lover refactor things in such a way that one thing is always calling another thing and even though the actual working business logic might be 12 lines which should have been linearly written, is spread across 5 files with objects being new'd up everywhere making it really hard to follow.
salary = 100
bonus = 10
total = (bonus
+ salary)
=> 100
Argh. The real example was more complex but you get the idea. Lint/Void: Variable bonus used in void context.
Web search for 'ruby void context' finds a bunch of WTFs.99% of the time when I get air dropped on a big piece of non-rails ruby software it's going to 1. be a headache and 2. earn me a tremendous number of billable hours trying to fix it.
A lot of dev/consulting shops that popped up after the advent of Rails began using it for absolutely everything and none of them knew what they were doing architecturally.
Agreed 100%. This practice pushed me out into Rust/Go and I don't see myself returning unless corrections are made.
I strongly believe Shopify, Stripe, Rails need to come together and eradicate this blight from the ecosystem if they wish to see new adoption. No other ecosystem finds this acceptable behavior and is actively discouraged.
> Ruby is probably the most expressive programming language on Earth
I wonder if there is a generally accepted quantifiable definition of programming language expressiveness. Here the author seems to equate it to closeness to natural languages.
In my experience, ruby code is easy to write, but hard to follow without being familiar with the code base and it's idom. A lot of the information required to understand it is passed through implicit context.
In my opinion, expressiveness is used as the catchall, je ne sais quoi whenever someone likes a language and needs another bullet point to put in the "Pros" column.
It feels like expressiveness is the quality of a language to allow the programmer to change the behavior of the language constructs themselves. Python __getitem__ would be a good example allowing the programmer to control [].
Modern framework are a bit better.
If it's an old project, that's a given but the culture is changing and community is slowly accepting terseness and performance as key factors to consider when writing new code.
Pretty much all the new-ish language have less boiler plate than C#/Java. Kotlin and swift are somewhat good.
Scala might be what you're looking for. (Or Haskell, but that's a bigger leap). You can write code that looks like Python/Ruby (some libraries are quite symbol-heavy, but you don't have to use them; check out lihaoyi's libraries for some Python-inspired simple interfaces), but everything's fully typed. And it has an excellent JavaScript backend that integrates with typescript/definitelytyped for using web libraries.
I think the main problem with C# is that even though you can write scripty “pythonic” C# since like 2010, people are still coding C# likes it’s early 2000s enterprise Java. The .NET library doesn’t help either, since it sets the “style guide” that many developers follow.
If there is such a definition it would not fit into these sorts of conversations. I love Ruby but every time I see someone mention Ruby "expressiveness" it is never mentioned as a subjective fact like syntax feature count. Instead it is always closely tied to emotional feeling, assumptions, and intuition. "Ruby's expressiveness means it just gets out of the way and lets me code." I've probably said that myself a hundred times.
OTOH it is great for DSLs and metaprogramming, gives a taste of what the LISPers have always had. Maybe that is a good definition of "expressive".
[edit: added thought about DSLs]
Because programming languages are, at the end of the day, for communicating with other people trying to make the computer do things. And people have different styles, ideas and preferences.
The computer doesn't care; it just wants some binary code to execute.
This is the counterintuitive genius of Ruby/Rails.
Ruby projects need an idiom and Rails gives you a good one. If a team knows Rails and its conventions they are a good ways toward developing a shared understanding of how to build (certain kinds of) software. You become acutely aware that writing code is an act of communication.
Other languages, ones with certain kinds of type systems and where there is more explicitness, can create an illusion that coders don't need to do the work to develop a shared mental model of the problem and how to solve it. As long as it compiles, everything's OK.
But I think there is always information being passed through implicit context and if it isn't handled correctly then quality will suffer, one way or another.
I can't say i agree. Projects need idiom reflecting the particulars of said project. Rails as a framework, shouldn't impose idioms to the whole project.
But more importantly from my view is that implicitness is not a requirement to any of the idiom that rails imposes.
> Other languages, ones with certain kinds of type systems and where there is more explicitness, can create an illusion that coders don't need to do the work to develop a shared mental model of the problem and how to solve it.
Here again, i don't agree. I would be surprise to find any language designer trying to replace the need of a shared mental model. IMO, the point of a type system and explicitness is to make the sharing of the mental model easier. The type system make the space of possible software smaller and therefore eaiser to grasp. And explicitness reduce the need to keep things in one's head.
I feel that some teams don't spend enough time discussing the mental model required to contribute well to their codebase. I feel that type systems can hide this, and that people creating the types for a project forget to make a coherent, understandable model (either this, or I'm too junior with my typing and don't yet have the skill to infer a model from the visible types).
I also think this model-building work has two related aspects:
1. it's hard to do and hard to describe
2. one of the best ways, in my opinion, is to build comfortable abstractions that reinforce the model.. but that's even harder to do. It's easier to "enforce" than it is to "educate"; types and patterns get used to enforce structure first, rather than to explain the structure. To be honest, I only have a light grasp on this myself... I've just seen a few cases where a codebases chosen abstractions and patterns of abstractions help to reinforce a structure, but don't help to explain anything about that structure or the problem it attempts to solve
I disagree pretty heavily. There are certain things where it may be valuable for a project to have it's own bespoke idioms or conventions (if it was doing something especially novel because that's what was necessary to solve the problem), but there's all kinds of decisions where this level of uniqueness actively harms a project, because it's not something that developers should be expending energy on. How to log data, how to deal with basic straightforward security concerns, how routing should work, how to connect to a database, how to run routine queries and get an object graph back are all boring problems with well established "good enough" solutions and no particular need to innovate, and expending cycles on those, either in initial development or when new people onboard is wasted time.
I can be pointed at almost any Rails repo and have a baseline understanding of where to find things on day 1. That is almost never the case with a Javascript project.
And this is a problem that a type system doesn't fix. A type system can't tell you that all ACL logic is in the reporting subsystem for some reason or prevent two components from managing user state in incompatible ways.
But every project will need to violate the conventions somewhere. If you just made all of the rules a baked-in part of the language you'd end up with a very inflexible language.
There's a lot of power in encoding some things at the human level instead of the machine level.
I doubt it, considering "expressive" tends to only ever describe something that's subjective.
I was going to make a similar comment -- it seems there might not be well agreed definitions of "expressiveness" here. I write most of my own projects in Haskell, precisely because I find Haskell to be extremely expressive, but I mean something different to "reads like English" (for which I think AppleScript is the closest example I can think of): to me, the expressiveness of the language is "how accurately can I describe my problem domain to the computer in a way that lets me naturally reason about it in code". I think this is mostly a function of the type system, not the particular syntax (e.g. allowing "?" in a method).
I am really interested in what other people consider "expressiveness" to mean, however.
Personally, I just can't do without a good type system anymore. I think Rust has spoiled me. I do miss Ruby's powerful reflection features though.
The author here really hits on exactly why Ruby is my favourite "get things done" language. Rust, Typescript, and Crystal are all things I've worked with, but nothing gets out of my way like Ruby. It feels like sketching? It's very expressive thanks to it's prose-y syntax, but also metaprogramming + reflection like you mentioned really makes forming an idea while developing possible, at least much more possible than most other careful correctness-ensuring paradigms.
There's nuance to these things, and nothing is the best really. But I think satisfying a borrow handler or a type checker does slow down that flow for me. I'd pick up a brush if I know what I'm making for sure, but a piece of chalk if I'm just wanting to get something working. Personal taste of course!
https://www.youtube.com/watch?v=Tml94je2edk&list=PLEx5khR4g7...
I do like Ruby for my own projects. But I hate working on Ruby with others. It becomes more messy faster and then all that supposed “getting out of your way” goes out the window and you have to know about 9 objects, one of which is an iceberg which will need to quack at some point.
Overall, it obliterates a programmers ability at local reasoning in code, which is where I really want speed.
I mean I personally believe maintainability is a combination of the language itself and the culture around it; Go is a great example of a language and culture aimed for readability and maintainability, eschewing cleverness. That is, the language is limited in how much cleverness you can write, and the culture is opposed to people trying.
Ruby is heavily influenced by Perl, with a focus on being enjoyable to write. And let me be clear: I loved writing Perl, back in the day. If I still used Perl, I'd still love writing it. But part of why Perl is so fun -- and why Ruby is fun! -- is that it's expressive and flexible, and you can do a lot of things you really shouldn't. This leads to people writing code that was fun to write, but will be difficult to maintain -- and the language's foundation rests on this concept, so the culture is (subtly?) encouraged to embrace it.
The problem with maintaining Ruby is that Ruby is heavily influenced by Perl.
My job as a programmer would be infinitely better if I could spend it programming Elixir all day, but there is basically no Elixir jobs compared to Typescript, Python, and Java.
But instead I hate my job working in Typescript all day solving problems that I wouldn't have to if I could just use Elixir.
Ruby use to an exception to this, but almost no one is building on Rails anymore compared to how many people build on React, and NextJS.
I'm talking about full stack application market, this is probably different for Rust users for systems, and Python for Data/AI.
Before I focused on iso js, I worked with EventMachine & even a little bit of Erlang. Would love to hear more about an alternate timeline if I stuck with Ruby & went down the Elixir route.
---
I used to work with Ruby & switched to js before TS to build isomorphic libraries & apps. TS has been beneficial imo though figuring out type inference for library code is a steep & time consuming learning curve & takes experience to figure out the edge cases. In the end though, having type inference work in library code is worthwhile for quickly developing apps. Re: expressiveness, the js/ts ecosystem suffers from mostly using camelCase, which has variable casing depending on the position of the name segment...which makes project wide searches for composed abstractions less reliable. I distilled the "tag vector" name convention to address this issue. Granted, Ruby having `?` & `!` available is as a terse & explicit expression of intent is nice.
> almost no one is building on Rails anymore compared to how many people build on React, and NextJS.
I got burnt out on Rails after the 3rd consecutive upgrade project for large codebases. I think the dominance of React has been detrimental to the js ecosystem as its bloated with the api being complex & unintuitive. It also took the JS framework guys a decade to figure out the MPAs are the way to go. I hoped people would have figured that out sooner. I got tired of the complexity & size of the reactive state management & ui libraries & recently wrote my own (rmemo & relementjs). I working on a vite alternative called rebuildjs with an Elysia/bun based app library called relysjs.
I loved how many Ruby community (other than Rails) had a commitment to create simple libraries. It seems like js framework communities have this drive to lock developers into their complex manifestations.
Sorry about the long comment. I have to get back to a major version update to ctx-core (general purpose contexts & utility functions) addressing type inference...which will make developing apps with these libraries more reliable & effective.
https://www.briantakita.me/posts/tag-vector-1-tag-vector-con...
https://github.com/ctx-core/rmemo
https://github.com/relementjs/relementjs
https://github.com/rebuildjs/rebuildjs
The point is that working in the elixir ecosystem gives any sohpisticated engineer _multiple_ superpowers.
- for architecture/system engineers, its the BEAM/OTP
- for frontend/fullstack devs LiveView is the best thing in our industry for like 95% of all use cases out there
- for data engineers/... having Livebook is like Jupyter on steroids, even better when linked to the production systems and Ecto is just outstanding as an ORM take, especially when comparing to much weaker options in the JS ecosystem, and don't get me started on orchestration or data pipelines with broadway
- for AI enthusiasts all the machine learning stuff is at your fingertips already, with bindings to the native libs also used by python but more ergonomic and a lot of QoL over python because of the strictly better BEAM capabilities + toolings
- ...
There are a few areas I can highlight above where the tech absolutely excels so its the best or one of the best options in general. The magic then is that Elixir is also like 80-90% in nearly everything else with the only 2 shortcomings being raw computation speed (native libs can help as usual, easy with rust) and deployment being more complex than Ruby/Python since you should link the server nodes (no-brainer with hosters like fly)
Ontop of that there is fluctuation and risk-avoidance, so using anything that is not absolutely mainstream is seen risky, because replacements might be a bit harder to get. Combine that with the usual mediocre preexisting employee base, and its obvious why by default the dumbest/broadest thing is everywhere in use, even though most people involved in the decisions aren't idiots.
There are roughly 2 types of places where you can encounter Elixir or other exotic tech:
1. The decision maker / CTO / tech lead actually has the autonomy and the personal interest in this tech so he is the driving force to level up and stand out. Only works in tandem with enough dev motivation in the team or it will backfire.
2. Startups/new tech insourcing departments, where a small circle of actually competent/motivated people got to choose the foundation and are by some miracle not blindsided already with "pick the cheapest and least risky" option but understand that tech can be the critical advantage needed.
Joker slot: be a solopreneur and use whatever you feel comfortable with. Thats the general escape hatch that is becoming increasingly popular it seems.
> The language is meant be joyful to use. [...] Everything else that Ruby is stems from this value.
This is important and underrated. I think many programmers have a bias that working on a difficult problem entails using a "real" programming language with sharp edges. I had some version of this bias for a long time until I started exploring the most recent generation of systems languages.
> Well written ruby code can often read like natural language.
I see where the author is coming from, but I find a healthy dollop of symbols to be very helpful for reading and understanding code at a glance.
> Feeling recognition in the language you’re programming is so powerful.
This is the feeling I had the first time I used Python, and later Rust. It's a wonderful feeling!
> [As] Kent Beck said at RailsConf in 2020, “Software design is an exercise in human relationships.”
Especially true given all of the components involved in supporting a language: compiler, docs, standard library, third-party libraries, package managers, frameworks, formatters, profilers, ...
All languages have some mantra that everyone repeats and that is the Ruby mantra. Personally I find Rust to be joyful because the type system, project and package management is so good and the end result is efficient without any extra effort. What I find joyful is getting things done well.
For me Kotlin is the sweet spot in programming language. You get a concise, readable language with world class tool support, static typing and the excellent performance of JVM + everything available in the Java ecosystem.
Ruby LSP already does quite a bit and started gaining go-to method abilities last month [2], though it isn't complete quite yet [3]. It's build on Prism, so promises to be more robust than past attempts. Shopify has been moving fast on improving things here.
The Rdbg integration works great too [4]. Just add a `launch.json` and VSCode can hook into the very nice capabilities Koichi Sasada has been adding to the official `debug` gem.
[1]: https://code.visualstudio.com/docs/languages/ruby
[2]: https://github.com/Shopify/ruby-lsp/releases/tag/v0.12.4
[3]: https://github.com/Shopify/ruby-lsp/issues/899
[4]: https://marketplace.visualstudio.com/items?itemName=KoichiSa...
I discovered the Ruby community at its peak around that time and fell in love with it! I started a new project at work with Rails and all my next jobs were Ruby jobs.
I experimented with Node when it came out but I think I'm burn out with everything related to JavaScript at this point. And the Ruby community, while still great, seems to be dying out locally. Meanwhile I've been doing some Rust side projects for almost a decade, and it looks like this will be my next language. Time will tell.
I still love Ruby though, I wish it was more popular, it's such a nice language.
(Does that make Ruby a good programming language? That's unclear to me; what's clear is that I have fun writing it.)
Perl felt like that in the 90s. Ruby (not Rails) seemed like it was trying to do a better Perl - better OO, better functional style, better syntax, etc. Both of them inspired a lot of things in other languages. And lots of people groan and complain about both of them today.
From what I can tell, a lot of complaints are from people who don't really like to code. Expressiveness means choice and that means thinking about coding instead of just following a "one way to do it" pattern over and over. Thoughtful coding, making something unique and making it easier than other examples, is also the kind of coding that LLMs can't do - they only munge together mediocrity out of what exists.
The complaints are from the poor suckers who had to maintain that "mirthful" code after it was written. Writing greenfield code is already fun in most languages; understanding existing code is already the hard part, and having to decode the "unique" thoughts of whoever came before you makes it worse.
I always understood Rust to be for low level “close to the metal” sort of software. Is it at a point where it’s suitable for writing web applications?
I know it’s “possible” with frameworks like Rocket, but I’d like to know if Rust is at a point where it can compete in the web app space with Rails, Go, Node/Express, etc.
It's a high-level language, it just has its own rules that encourage correctness and prohibit sloppiness (which is a de-facto standard in the webdev industry, especially when prototyping rapidly by throwing shit at the wall and then letting whatever stuck live in production until it's no longer manageable).
For better or worse, Rust simply doesn't forgive a lot of things that are easy to do elsewhere, like not caring about less probable scenarios.
And, again, for better or worse, you also have to satisfy the borrow checker, where in other languages there's simply no such thing. Which is sometimes easy as calling clone() (not always a good idea), but sometimes can be quite a headache thinking about value lifetimes and how you just can't have something somewhere else (which can be super subtle so you wouldn't normally think about it in other languages with GC).
I think the language is fine, but the thing that really makes me stick with Ruby is the ecosystem and the culture it fosters. The article also makes that point.
I have yet to see a programming community (around a language) that is as encouraging and nice as the one around Ruby.
Agreed 100%. It's like the opposite of the javascript community where I feel like the leaders of various camps are constantly trying to tear each other down about the best way to do things as they fight for developer mindshare/marketshare. Maybe I just see too much JS drama on twitter. Anyway, the Ruby community is so wholesome and a breadth of fresh air in the modern tech world.
I don't use it much anymore, because most companies seem to have moved away from it. But, I do miss it and wish we had something similar that could encouraged that level of shared understanding.
irb(main):028:0> value = false or true
=> true
irb(main):029:0> value
=> false
That really makes me want to write it off entirely. It’s hard to think of a situation where this is the right behavior.For some more explanation see https://graceful.dev/courses/the-freebies/modules/ruby-langu...
I’m not saying that it isnt valid to criticize these things: first time use of a language matters, people work in multiple languages, it is good to be intuitive.
But in practice, these things aren’t problems for people who work regularly in these languages, so I personally find them to be quite low salience.
I recognize that this is sort of similar to the claims the hypothetical user of php makes in “php is a fractal of bad design”, but personally I find think the issues are of a different nature.
But then is being surprised by `["0","1","10"].map(parseInt) // => [0, NaN, 2]` in JavaScript a sign of bad language design or does it just mean that you don't have enough experience with the language yet?
however,
irb(main):001:0> value = false or true
=> true
irb(main):002:0> value
=> false
irb(main):003:0> value = false || true
=> true
irb(main):004:0> value
=> true
irb(main):005:0>
I believe if you use the `||` operator instead of `or`, then things just work out fine. I agree it is really annoying. But I am pretty sure if you use a tool like RuboCop https://github.com/rubocop/rubocop (a static code analysis tool) then it will catch bugs like this. Note that I am not recommending Ruby. But in my experience if I want to work with a language and it has a community style guide and a linter that enforces it, it will save me some heartache.Just editing to add this reference: https://rubystyle.guide/#and-or-flow
I’m not looking for heroes to worship, just good tech.
It’s with the community that insists that DHH’s software design edicts are The One True Way. Instead of software design being a thing you can use to make your life better, we are to believe that Rails Is All You Need. And think at the time it was the framework to use, so it attracted a lot of people who believed software dev should be “do what the thought leader says.”
No idea what it’s like now, but it truly felt like people saying those things out of tribal affiliation more than technical acumen. And that is a recipe for stagnation.
That said, I still respect the tech and the creators a lot. I just don’t share the values at all.
I tend to agree there's much cruft in the Rails space, but I also hardly ever interact with the Rails crowd.
RSpec.describe Ticket do
context 'when the ticket is closed' do
it 'emails the requestor with a confirmation' do
...
end
end
end
I have no idea what's going on here.I get that there are some blocks of code, though it was the indentation that told me that; "do" and "end" feel super verbose and bleed into the important parts of the code for me.
Why is Ticket capitalized? Is this a variable? An object that we're about to work on that's coming into scope?
`context `when the ticket is closed'` feels like it's setting me up. Is this an if block? Or is it some fancy way to set a listener on a property? Is this setting up a callback that'll persist across runs of the program? Or is this just a method named with spaces that could do anything?
`it 'emails the requestor with a confirmation'` has got to just be a method name. But what is "it"? What was "context" in the previous one? And why does "emails the requestor with a confirmation" need to be a block? What happens in there? Is this just setting up some kind of call stack like thing that provides context all the way down?
None of this is intuitive. And the impression that I get is that the author is calling methods with spaces in their names and unclear block semantics "expressiveness".
do ... end is how you create no-argument functions similar to () => { ... } in JavaScript. There's some technical nuance but that's the spirit of it.
In Ruby you can call methods without needing to place parentheses.
So this roughly translates to the JavaScript code:
RSpec.describe(Ticket, () => {
context('when...', () => {
...
});
});This specific code describes a test that uses the RSpec testing framework. In RSpec, "context" and "it" are just functions that you call to describe chunks of tests and individual tests respectively.
Some Rubyists would say that the combination of these syntax rules (do-end and optional parentheses for function calls) allow you to make "domain specific languages" (DSLs as they're known in the community) without needing to write your own parser.
I think it's pretty plain to see the impact in expressiveness if you look at the equivalent Python unittest suite. The Python equivalent would have classes that inherit unittest.TestCase and methods named like test_when_ticket_is_closed.
In Python land, classes and method declarations are repurposed because the syntax is what it is. In Ruby, you can practically make your own faux keywords by abusing do-end to accept nested blocks.
I can mentally parse this (Ticket is a class name, context and it are methods that accept blocks, etc etc), but I have real hard time finding where this stuff is actually defined to see how it works underneath. And I always peek under the hood for the implementation nuances - documentation is always incomplete, so frameworks' and libraries' source code is my go-to documentation.
In other languages, imports are explicit and are local to file (and people outside of ML and quick-and-dirty one-off scripting tend to recommend breaking one's fingers for a heresy of `from foo import *` - with some obvious exceptions, of course), so I always can see where exactly stuff comes from.
In Ruby world there's this weird love for autoloaders, or some sort helper.rb with tons of require/include directives and so on, so very rarely I see a Ruby code that links stuff explicitly. Paired with extreme commonality of metaprogramming, reopening classes to extend them, all paired with the dynamic nature of the language, this kind of irks me as ideologically "wrong" (as in "not to my liking").
Surely, modern IDEs sort of figure out most of the stuff, but I was trying out Ruby quite a long time ago, before language servers and stuff became mainstream and widespread, and still can't shake off this impression.
> ...
> None of this is intuitive.
Why would anyone expect that a highly-context-dependent code snippet (in this case, a test suite written in the testing framework's DSL in a language that they both don't know and is different from the languages they usually write) would be "intuitive"?
Frankly I feel like many programming language discussions are tainted by the participants tying their own intelligence to whether or not they can understand something at first glance (and if they can't, it must "obviously" be the thing that is wrong). This makes about as much sense to me as concluding that my utter confusion when glancing at a page of Portuguese poetry is somehow a problem with the language itself.
The codebase I work on has a bunch of Ruby scripts with "highly-context-dependent" bits of code in them that I have to occasionally dive into to find out why they're failing.
Elsewhere in this thread people are complaining about how few Ruby jobs there are. I'm eliminating one of those jobs.
I replace each script with Python, or Java / Kotlin. I don't have the time to learn about all of these contexts. And I can't afford to hire people who want to specialize in Ruby. And typical engineers can't intuit about them, so nobody can properly support them. But those engineers can typically intuit between Java and Python just fine.
I understand that there is Portuguese poetry and that I may not understand it. But I don't mix it into a book of English poetry.
The author is saying that this code
> reads exactly like how a person might talk about what they want to test
A person WOULDN'T talk about testing a Ticket like what you're describing with implementation details
"Ticket is a class, which will have a method of _this_ and I will call that method, which will then do _that_, and I will take the return of that method and call put it in a variable, that will be a constant..."
A person WOULD talk about testing a Ticket high level like this
"Let's describe a Ticket. When the ticket is closed, it emails the requestor with a confirmation."
Now listen, obviously not being familiar with the syntax makes reading code harder, but a programming language isn't designed so that people can just look at the code and learn the syntax from looking at the code. That wouldn't make sense.
With that said, I struggle to see some of your points listed as non-intuitive. All of your comments are related to not understanding the implementation details of this code. And that's just not what's being talked about here.
I often see complaints about this when people are uncomfortable accepting _syntax_ that clearly tells them _what_ will happen, but if they don't understand HOW that happens, they don't like it.
Here's another Rails example, for validating a model ``` class Person < ApplicationRecord validates :name, presence: true, uniqueness: true end ```
Can you seriously say that you don't understand that the name of the Person will be validated if it is present, and if it is unique? I doubt that very much.
Can you say that you have no idea how it's implemented? Yes. You don't know how something might be implemented in a language you don't know. But, does that really matter when talking about expresiveness?
> 'emails the requestor with a confirmation' .. 'when the ticket is closed'
The compiler can figure out the other bits. do .. end is { .. }The guiding theme for Ruby appears to be "let's give people a huge number of ways to write unreadable code" and it reminds me of JWZ's classic rant about PHP, "a fractal of bad design."
You couldn't really find that many quirks, edge-cases and foot-guns in Ruby, could you?
It's one of those things where you either love it or hate it. The first moment I saw a question mark denoting a boolean, I fell in love with Ruby right then and there, but I also perfectly understand why people hate that. Same with stuff like `unless`, whenever I use a different language I'm always tempted to make a small utility class with all the Ruby niceties like `unless`, and that's another thing I can completely understand not liking.
A question mark is useful on predicates when they are nouns. For instance type tests. Here is why: widget(x) looks like a constructor: it's making a widget out of x. If we add a question mark: widget?(x) then we know it's a test whether x is a widget. We have other choices there like is_widget(x).
"Supports feature" has a verb and object. The verb is not in imperative form; it's in a statement form. A question mark is not required.
You’re always running Ruby statements in some scope, so there’s always an object you’re running methods from. That makes your constructor and reflection examples a bit odd for Ruby.
Constructors would be a method on a class, normally the Class instance method #new: Widget.new(x)
Checking if y is a Widget would mean calling a method on that instance, normally the Object instance method #is_a?: y.is_a?(Widget)
I think that’s adequately distinct.
One thing I don't miss that a significant part of this short article, is RSpec. I was into it at first but it just becomes a burden. When you're writing test descriptions in English, there is very little value gain to gain from the ceremony RSpec has. As for assertions, it turns out that if you're a programmer, `assert a == b` is just as readable as `expect(a).to eq(b)`. Yes, the latter does make it easier to get the arguments in the right order for the output, but that's about it. Otherwise, it's just "neat." You end up thinking a bit too much if you are doing things "the rspec way" which is not something I want in my brain when writing tests. It's also more to learn for onboarding people. Stuff like `expect(a).to be_present` really means `assert a.present?` are head scratchers that I just don't see any value in other than being neat.
RSpec is definitely a cool idea and a very well-designed project, but un-necessary by me. It's been 3 years since I've used it daily and haven't found the tests I'm writing these days to be any less readable.
The fact that Sorbet has not seen adoption outside of Shopify is also somewhat telling.
It’s disappointing, because Ruby needs this. Modern tooling has moved in the direction of typed languages, and Ruby’s tooling has suffered comparatively. Outside of RubyMine it’s still difficult to get “jump to definition” to work consistently, and in 2023 this is a rather embarrassing strike against the language.
Then I accidentally learned Python one afternoon and never thought about Ruby again. I'm sure Ruby is better than Python in dozens of ways, much as alternate keyboard layouts are better than QWERTY (and Rails is probably better than Django.) And I'm still mad at Python for setting fire to billions of lines of 2.7 code that people had written. But I'm still using Python.
A language is one possible notation of thought.
I like to be productive with my thoughts. I tend to do proof of concepts in Python then port to C or in Java directly.
I remember learning about Ruby when posted on digg and there was a new tool coming out all the time.
It seems like Rails really caused Ruby to fall out of favor around 2010 so it's interesting to see some positive Ruby articles here lately. Looks like there's been a good amount of performance improvements as well as new language features added over the last decade - perhaps we're seeing a bit of a Ruby revival?
It should never be forgotten that the ultimate purpose of programming languages is to make specific usable programs. It's sort of fashionable to ignore performance but in reality it's always an issue of some kind. And the fact remains that the price paid for Ruby and Python's features is severe performance degradation. That's improved over time but as far as I know it remains a real problem. One shouldn't let admiration for language features supersede user experience and resource utilization.
There are of course places where it isn't appropriate to use a Ruby or a Python, sure. So don't. There are plenty of others where sniffing at them on the grounds of performance is making up a guy to get mad at.
This isn't even some kind of well-actually gotcha, it's firmly a mainstream, obvious thing.
Python looks much more uniform in this sense and it is very powerful. Maybe not as good for DSLs, yes, but for general coding it keeps itself very understandable.
check fastapi, sqlalchemy, or any ML library, what a mess...
But most ot the time you find for loops, comprehensions and unsurprising code.
> Well written ruby code can often read like natural language. Features like predicate methods even give up punctuation. [...] This is often why ruby programmers don’t like comments. In most cases, the language makes comments unnecessary.
churns my stomach. Good comments are rarely about saying what the code does, they're about explaining _why_ the code does what it does. Regardless of how comprehensible ruby code is, it doesn't change the need for comments. If I had a nickel for every time I gave a commit a -1 review because it had too few comments or the wrong kind of comments or lacked a "why" commit message, I'd be a rich man.
If I had to guess, the anti-comment attitude comes from consultants who re-implement the same CRUD webapp on Rails over and over. Sure - no surprises, no comments, fine.
I would even go a step further : I prefer a cryptic code with good comment explaining what the code does and why it does what it's trying to do vs a very clear code with comment on How the code works.
Commit messages are such a good place to explain code and I rarely need the same explanation in a comment. I worry though about the fragility of the connection between (a) the source code and (b) the important commits that explain why the code is there.
Perhaps there are some very good UIs out there already that show something a little more intelligent than than the most recent commit from blame?
Another idea might be to attach blame-like functionality to the syntax tree so that the explanatory messaging can be tracked at the function level, rather than as lines of source code. Can others here can point me in the right direction for either a more tangible description of this feature, or an existing research?
What's even better than comments, is a compiler that actually guarantees you're gonna be given something of the type you expect.
This only makes sense if we're in the school of thought that writes
i = i + 1 // increment i by one
The more charitable interpretation is that they're pointing out the developer ergonomics of things like predicate methods remove the need for comments that might otherwise be reasonable to insert. Whether or not that is true is subject to reasonable debate, but it's definitely a more nuanced opinion than "Ruby's predicate idiom means I don't have to document my obvious code."
Hence, lots of Ruby code is a sequence of trivial declarative blocks that look like natural language.
In these parts of the code, comments are redundant. I think that is the OP's point. Other parts definitely need them.
Is mathematic better when it reads like English? I personally don't think so. Yeah it's a bunch of symbols and conventions to learn but it ends up being more precise.
I dont have anything to back it up :) I just think that natural language is almost by design messy and imprecise so I'm not really looking for that in my code.
Why does this function exist? Why was it implemented this way and not that way? Sometimes these are really important bits of info. Here's where this approach breaks down. Here's a sharp edge that we hope to remove someday. This part here incurs some technical debt but this is why we did it.
In order to really maintain a codebase (meaning: safely fix bugs and add new features), you need to have an understanding that at least approaches that of the person who wrote the code originally, and good comments (e.g. level of intent comments) can help you do that. This is true even for your own code that you're returning to after some amount of time.
Comments are not checked by compilers, linters, code formatters, unit tests, integration tests, or any other automated tools for correctness, consistency, and good formatting. They rot quickly.
For example, if code is operating on a list, and then it sorts that list and passes it along to some other code, I can see perfectly that the list will be sorted. But that doesn't tell me _why_ it needed to be sorted in the first place. Often that is not knowable except by reviewing things outside the scope of the code in question, or outside any code at all. A comment is essential in that context.
Consider the overhead of a comment - first you need to read the comment because they are always above the code so that's what your brain will do. This will prejudice your reading of the code.
Then you need to read the code while trying to block the comment prejudice out of your mind.
Then you need to sort of mentally diff the comment and the code, and if the comment is wrong (spoiler...) decide if you are going to delete the comment (the best choice, unless you get a code reviewer who likes comments) or fix the comment (the worst choice, a waste of time, but maybe the only way to get it through code review).
Now consider code without a comment: you just read the code.
Sometimes you just need comments.
"Why" comments need to go onto code that make some kind of arbitrary decision. Or code which is integrated with processes that are happening elsewhere and has to behave in a way that complements those behaviors. Otherwise the reader would have to read that code together with all those other pieces (or specifications) in order to understand it. Code that is written in a certain way (particularly, an unusual way) for some reason that is not obvious deserves a "why" comment. E.g. "due to a compiler/library bug [give specific version info], we cannot just do this: ...".
Code that doesn't make arbitrary decisions or integrate with things elsewhere, or make unusual coding decision for reasons, often doesn't require "why" comments.
The author of the article hastened to add:
> And when you do need [comments], it’s often when you’re doing a very specific or obscure thing that requires context to understand. It’s clear from the code why you need a comment in that case.
It's clear from the code why you need a comment, because it's a "why" comment.
I say this as someone who wrote Ruby for 6 years and loves the language for its other features (e.g. everything is an object, functional programming, DSL-capabilities)
In a different language you might deal with more boilerplate (getting some data access object for feature status, passing the db connection, extracting the subscription id, etc), or less clear naming schemes (importing namespaces, aliasing them for convenience, your subscription might come from some one letter temp variable), or something else. At this point the “why” comment might be not that different from the actual ruby code.
Note that we only see one line of ruby code; the parent method name, or the if body would have more context.
Then again, ruby and rails make it especially easy; perhaps the amount of conventions helps. Other things, like the ‘?’, clean syntax and focus on structure (OO), help too.