Very clear APIs and syntax(with the possible exception of blocks which can be weird because they aren't quite functions), and tons of raw metaprogramming powers.
You can argue it sacrifices too much of the other things to deliver on these things, but it's hard to argue against it doing well at what it optimizes for!
Pieter Levels writes his startups in PHP and hasn't had a performance bottleneck so far. For most applications, the performance of the language won't be an issue. I personally wouldn't pick PHP for any of my own projects, but Ruby I'd pick any day.
That's the reason why we can't have nice things.
I work at an enterprise running Rails (you've heard of us, if you're in North America). Discussions about rails abound, and my opinion has distilled into "I love writing Ruby, I loathe reading it"
man, people are still parroting decade old, incorrect talking points I see.
Is ruby as performant as C, probably not, although, actually, in some cases, it outperforms C -> https://railsatscale.com/2023-08-29-ruby-outperforms-c/
One of the largest ecommerce apps in the world runs ruby, shopify. Ruby now has a JIT, there has been insane effort put into making ruby faster.
Bit of a clarification after reading the article - that article demonstrates a pure-Ruby implementation [0] outperforming a C extension [1], which is not what I had originally expected when first clicking on the link.
[0]: https://github.com/tenderlove/tinygql
[1]: https://github.com/rmosolgo/graphql-ruby/tree/master/graphql...
Metaprogramming is Lisp's canonical super power. Ruby is going to win out on tasks where it has built in syntax, like matching regular expressions.
But once you get to metaprogramming Lisp macros are going to give Ruby a run for its money.
I will say one of the under appreciated aspects of Ruby is the consistency of its semantics, where everything is message passing very much like Smalltalk.
I love me some Clojure, and its macros aren’t bad or anything, but I feel Scheme (and Racket) has the most elegant metaprogramming.
Other than that, how was the play, Mrs. Lincoln?
Also, add readability and maintainability to that list, and scaling to a large codebase. And good debugger support.
rubygems.org also has decided to, rather than fix on existing problems, eliminate all former maintainers and instead put in Hiroshi Shibata as the solo lead - the same guy who keeps on writing on different github issue trackers how he does not have time to handle any issue requests for low-used projects. Wowsers.
I wouldn't be as much in love with programming, if it wasn't for Ruby. And although I use many other programming languages these days, Ruby will forever have a special place in my heart.
Ruby, and Ruby on Rails is a treasure trove of little handy bits you can use if you just know where to look. I really miss some aspects of ruby (I just don't have a chance to use it these days).
You find the same thing with JS to an even higher degree, but there's always 10 options in NPM and they all need to be updated every year otherwise the other 20+ packages you depend on can't be upgraded. There's a stark contrast in maintenance overhead and DX between frontend and server side.
Even the rails JS libraries are very stable. Hotwire's Stimulus hasn't had a release since 2023 and it always works just fine. https://github.com/hotwired/stimulus/releases
Glad to see it's getting love on here recently.
Here's a screenshot from inside FreeCAD:
https://f.toi.sh/rubygem-screenshot.png
A nice manifold solid:
https://f.toi.sh/rubygem.3mf / https://f.toi.sh/rubygem.stl
A terrifying non-manifold FreeCAD mess: (requires surface WB)
Having done mostly TypeScript and Elixir lately, I had forgotten things could be so succinct yet so clear. The combo of modern (to me) Ruby's lambda syntax (in the .map call), parentheses-less function calls, the fact that arrays implement <=> by comparing each item in order, that there's an overloadable compare operator at all, having multiple value assignments in one go... It all really adds up!
In any other language I can think of real quick (TS, Elixir, C#, Python, PHP, Go) a fair number of these parts would be substantially more wordy or syntaxy at little immediately obvious benefit. Like, this class is super concise but it doesn't trade away any readability at all.
Having learned Ruby before Rails became commonplace, with its love for things that automagically work (until they don't), I had kinda grown to dislike it. But had forgotten how core Ruby is just an excellent programming language, regardless of what I think of the Rails ecosystem.
Some of those languages would have you deal with the problem of allocating multiple arrays in the heap just to compare three numbers. Or give you tools to outlaw passing invalid strings to AppVersion.new (quick: what is the comparison between AppVersions "foo" and "bar"?).
Plus you have very few tools to ensure code remains beautiful. I've worked with Ruby for close to two decades, almost nothing in the real world looks that clean. Take a look at the Gem::Version#<=> implementation that the article talks about: https://github.com/ruby/ruby/blob/master/lib/rubygems/versio...
from dataclasses import dataclass
@dataclass(frozen=True, order=True)
class AppVersion:
major: int = 0
minor: int = 0
patch: int = 0
@classmethod
def from_string(cls, version_string: str):
return cls(*[int(x) for x in version_string.split(".")])
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
Before dataclasses you could've used namedtuples, at a loss of attribute typing and default initializer: from collections import namedtuple
class AppVersion(namedtuple("AppVersion", "major minor patch")):
@classmethod
def from_string(cls, version_string: str):
parts = [int(x) for x in version_string.split(".")] + [0, 0]
return cls(*parts[:3])
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}" @functools.total_ordering
class AppVersion:
def __init__(self, version_string):
parts = [int(x) for x in str(version_string).split('.')]
self.major, self.minor, self.patch = parts[0] or 0, parts[1] or 0, parts[2] or 0
def __lt__(self, other):
return [self.major, self.minor, self.patch] < [other.major, other.minor, other.patch]
def __eq__(self, other):
return [self.major, self.minor, self.patch] == [other.major, other.minor, other.patch]
def __str__(self):
return f'{self.major}.{self.minor}.{self.patch}' >>> from packaging.version import Version
>>> Version("1.2.3") > Version("1.2.2")
True
>>> Version("2.0") > Version("1.2.2")
True record AppVersion(int major, int minor, int patch) implements Comparable<AppVersion> {
public static AppVersion of(String version) {
var array = Arrays.copyOf(Arrays.stream(version.split("\\.")).mapToInt(Integer::parseInt).toArray(), 3);
return new AppVersion(array[0], array[1], array[2]);
}
public int compareTo(AppVersion other) {
return Comparator.comparingInt(AppVersion::major)
.thenComparingInt(AppVersion::minor)
.thenComparingInt(AppVersion::patch)
.compare(this, other);
}
public String toString() {
return "%d.%d.%d".formatted(major, minor, patch);
}
}Admittedly, I have never done a lot with Ruby, but I have done some Rails and I tried for a few months to use it for simple “shell” scripts, and the language never felt beautiful or elegant to me.
Admittedly, I come from a strong functional programming background, so it is entirely possible that my brain sees “it’s not really a ‘functional’ and therefore I don’t like it”, but I do like Rust (even when I write it very imperatively) and I even kind of like modern Java.
Dunno, I will admit that I am weird, my favorite language is Clojure and I know that that one is an acquired taste :).
Scala would like to have a terse say about this... :-)
I like when parens/brackets are reliable wrappers for chunks of code. Like being able to ‘vi{‘ in vim to select a function body. Or ‘%’ to jump to the matching paren.
Do you find the language more readable without it? Less visual noise?
I don’t feel strongly about it, but you gotta admit that this is remarkably easy on the eyes yet also easy to follow:
parts = version_string.to_s.split(”.”).map(&:to_i)
The Elixir equivalent, likely a series of pipes, would be just as easy to follow but substantially more to read, more symbols to parse etc. I don’t feel like this here line of Ruby makes any sacrifices in understandability compared to this Elixir port we’re both imagining.Good point about grepping though.
Why thank you! :D
from dataclasses import dataclass
@dataclass(order=True)
class AppVersion:
major: int = 0
minor: int = 0
patch: int = 0
@classmethod
def from_string(cls, version_string):
parts = map(int, str(version_string).split('.'))
return cls(*parts)
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
All languages steal from each other, so on a long enough time scale, it makes sense they kinda converge. parts = version_string.to_s.split(”.”).map(&:to_i)
Is it to_string? Isn't version_string already a string?I think it's a safety measure in case the argument passed in is not a string, but can be turned into a string. Safe to assume that calling "to_s" on a string just returns the string.
It's also a quite bad practice to my eye.
A problem is that ruby lost many developres; rails too but it is by far the biggest driver in ruby. And this creates problems, because it overshadows the remaining ruby developers.
It's syntactic sugar for what Ruby does with a lambda, but fundamentally the purpose is to extract a method from the input. Python has that in the standard library, as `operator.attrgetter`. But also in Python, you generally convert by passing to the type constructor rather than calling a method; so you can just use that directly.
> parentheses-less function calls
Only methods are called here, not plain functions. You can get this effect in many other languages by defining properties instead of zero-argument methods.
> the fact that arrays implement <=> by comparing each item in order
This is also done in Python, and probably many other languages.
> that there's an overloadable compare operator at all, having multiple value assignments in one go
Idem.
> In any other language I can think of real quick (TS, Elixir, C#, Python, PHP, Go) a fair number of these parts would be substantially more wordy or syntaxy at little immediately obvious benefit.
A relatively direct translation looks like:
import functools
@functools.total_ordering
class AppVersion:
def __init__(self, version_string):
self.major, self.minor, self.patch, *_ = map(int, str(version_string).split('.'))
def __lt__(self, other):
return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch)
def __eq__(self, other):
return (self.major, self.minor, self.patch) == (other.major, other.minor, other.patch)
def __str__(self):
return f'{self.major}.{self.minor}.{self.patch}'
You don't need any `end`s, but you don't (in 3.x) have the convenience of a direct `<=>` analog (it used to be `__cmp__`). The actual integer conversion function could be done differently, of course, to handle invalid values (I don't know why the Ruby code is doing the `|| 0` business; `to_i` already takes care of that AFAICT).Although the rough ecosystem equivalent of Gem::Version (https://github.com/pypa/packaging/blob/main/src/packaging/ve...) does much more sophisticated parsing. And you could also get the comparison logic by leveraging `collections.namedtuple`, `typing.NamedTuple` (but changing the initialization logic isn't so neat for these immutable types), `dataclasses.dataclass` etc. as in js2's reply.
Nitpick: technically `Gem::Version` is part of `rubygems`, and while `rubygems` is (typically) packaged with Ruby, it's actually entirely optional, so much so that `rubygems` actually monkeypatches† Ruby core's `Kernel` (notably `require`) to inject gem functionality.
MRuby has none of it, and CRuby has a `--disable-rubygems` configure flag.
Back in 1.8 days, you even had to manually require `rubygems`!
† https://github.com/ruby/rubygems/tree/4e4d2b32353c8ded870c14...
* default libraries (these are maintained by the Ruby core team, delivered with Ruby, and upgraded only as part of Ruby version upgrades.)
* default gems (these are maintained by the Ruby core team, delivered with Ruby, not removable, can be required directly just like default libraries, but can be updated separately from Ruby version upgrades.)
* bundled gems (these are gems that are delivered and installed with Ruby, but which can be upgraded separately or removed.)
Rubygems is a default gem. [0] It used to not be part of the standard library, but it has been since Ruby 1.9, released in 2007.
[0] see, https://stdgems.org/
...There are presumably many other ways to solve that problem, but still.
That's not a nitpick, that's paraphrasing ;)
> It used to not be part of the standard library, but it has been since Ruby 1.9, released in 2007.
That's the mention of 1.8 I made, but it's a bit more complex: it still can be excluded, in two ways:
- at build time via `configure` (and then it's not even there)
- at run time via `--disable-gems`
The interaction between rubygems and ruby core is surprisingly small.
> Rubygems is a default gem
It is not! See `lib/ruby/gems/3.3.0/specifications/default` in any Ruby install, or in source the absence of _any_ gemspec for `rubygems` while there is for bundler[0].
Instead it's, as you mentioned, a default library.
The very principle of gems is that you can have multiple versions of a gem separately installed (to the point of allowing only one to be visible at any one time, activated by `rubygems`-provided `Kernel.gem`). The implementation of that is done by `rubygems` itself so if it were a gem, one would not be able to activate `rubygems` without `rubygems` itself...
This is also why it can be special-case upgraded only via the very special `gem update --system`, which downloads a "gem" named `rubygems-update` (not `rubygems`); scare quotes because it's using the gem format and infrastructure mostly as a delivery mechanism, not by being an _actual_ gem[1] in the traditional sense (well it is a gem, but a gem of an updater, not of `rubygems` itself).
When updated, the new copy of rubygems is installed in `site_ruby`, because the load path is the only mechanism available to define location priority (`ruby --disable-gems -e 'p $LOAD_PATH'`).
Fun fact: the only thing that "prevents" a file present it `${PREFIX}/lib/ruby/3.4.0/rubygems` to not be `load`ed or `require`d is merely that new code in `${PREFIX}/lib/ruby/site_ruby/3.4.0` shall not make reference to it, but it's all perfectly visible otherwise.
docker run --rm -it ruby:3.4 /bin/bash
gem update --system
ls -ld /usr/local/lib/ruby/3.4.0/rubygems /usr/local/lib/ruby/site_ruby/3.4.0/rubygems
echo 'p __dir__' > /usr/local/lib/ruby/3.4.0/rubygems/foo.rb
ruby -e 'p $LOAD_PATH; require "rubygems/foo"'
[0]: https://github.com/ruby/ruby/blob/v3_4_7/lib/bundler/bundler...[1]: https://github.com/ruby/rubygems/blob/v3.7.2/hide_lib_for_up...
it's so typical of ruby culture "haha, what if I do this silly thing" and then that gets shipped to production
https://github.com/ruby/rubygems/blob/v3.1.6/lib/ubygems.rb
https://github.com/ruby/rubygems/commit/8933115bff09402f6baa...
https://github.com/ruby/rubygems/commit/7ac54d5bb3411233b405...
I'd love to see a lot more writing and advocacy around Ruby, and not Ruby/Rails. I don't use Ruby/Rails! I use Ruby. And I suspect a lot of folks who have left Ruby behind over the years might not realize some (many?) of their gripes are not with Ruby in general, but Rails in particular.
$ txr -i version.tl
1> (equal (new (app-ver "1.2.003")) (new (app-ver "1.2.3")))
t
2> (equal (new (app-ver "1.2.003")) (new (app-ver "1.2.4")))
nil
3> (less (new (app-ver "1.2")) (new (app-ver "1.2.3")))
t
4> (greater (new (app-ver "1.2")) (new (app-ver "1.2.3")))
nil
5> (tostringp (new (app-ver "1.2.3.4")))
"1.2.3.4"
6> (tostring (new (app-ver "1.2.3.4")))
"#S(app-ver str \"1.2.3.4\" vec (1 2 3 4))"
Code: (defstruct (app-ver str) ()
str
vec
(:postinit (me)
(set me.vec (flow me.str (spl ".") (mapcar int-str))))
(:method equal (me) me.vec)
(:method print (me stream pretty-p)
(if pretty-p (put-string `@{me.vec "."}` stream) :))) 1> (hash)
#H(())
2> *1
#H(())
3> (set [*1 (new (app-ver "1.2.3.0004"))] 'mapped)
mapped
4> *1
#H(() (#S(app-ver str "1.2.3.0004" vec (1 2 3 4)) mapped))
5> [*1 (new (app-ver "1.2.3.4"))]
mapped
6> [*1 (new (app-ver "1.2.03.4"))]
mapped
7> [*1 (new (app-ver "1.2.02.4"))]
nil raise "check if monkeypatch in #{__FILE__} is still needed" if Gem::Version.new(Rails.version) >= Gem::Version.new("8.0.0")
This will blow up immediately when the gem gets upgraded, so we can see if we still need it, instead of it laying around in wait to cause a subtle bug in the future.Right. I think I found it on stackoverflow.
The question is: why does the official documentation not mention this, along with guides?
Answer: because documentation is something the ruby core team does not want to think about. It is using scary language after all: English. The bane of most japanese developers. Plus, it is well-documented in Japanese already ... :>
Versions numbers can go to 10!?!
And yes.. without ecosystem/libraries, everybody’s just creating their own thing over and over again and there’s not a coherent way of thinking
The Joy of Ruby
difflib is probably my favorite one to cite.
Go see for yourself: https://docs.python.org/3/library/index.html
The benefit there is that their quality, security, completeness and documentation are all great!