top | item 45849528

Ruby already solved my problem

262 points| joemasilotti | 3 months ago |newsletter.masilotti.com

126 comments

order

adverbly|3 months ago

If you ignore performance and mathematical elegance and safety and just look at how much a language lets you get away with from a productivity standpoint, I think Ruby is a pretty standout winner and nobody else even comes close really...

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!

tartakynov|3 months ago

Let's be honest, when you're starting a startup, Ruby's performance won't be a bottleneck until much later, when you're successful and get tons of usage - at that point you can afford to hire someone to fix it. Your productivity will be a bottleneck from the very beginning.

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.

jnovek|3 months ago

I love writing Ruby. It’s one of the most pleasant and elegant languages I’ve used… but the footguns it comes equipped with rival those of Perl.

wutwutwat|3 months ago

> if you ignore performance

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.

jimbokun|3 months ago

Various Lisps can give it a run for its money, depending on the problem.

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.

ramon156|3 months ago

What about PHP/Symfony? I have more experience in that and, after trying rails out this week, so much was overlapping.

chihuahua|3 months ago

"If you ignore performance and safety..."

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.

shevy-java|3 months ago

Right. But ruby also has awful crap. The documentation - look at opal, webassembly and many other projects in ruby. The documentation is just total garbage.

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.

iagooar|3 months ago

Ruby has a lot of these hidden gems (pun intended).

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.

netghost|3 months ago

A long while back I wrote a bunch of articles covering some of the standard library: https://snakeshands.com/series/ruby_standard_library/

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).

matltc|3 months ago

Agreed. Was looking around for STL files so I could print a ruby and put it on my desk.

Glad to see it's getting love on here recently.

skrebbel|3 months ago

Unrelated side note, but I haven't written any Ruby in maybe 15 years or so and dammn I forgot how elegant the language is at its core. The author's AppVersion class is so nicely done, it's nuts how succinct eg the compare implementation is.

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.

dlisboa|3 months ago

Ruby trades away quite a few things for readability. It's beautiful but a lot is being hidden.

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...

js2|3 months ago

Challenge accepted:

    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}"

vault|3 months ago

Like you, I remember 15 years ago when I decided to solve Project Euler in Ruby, a completely new language to me. I still remember the joy I was feeling when I started coding with this new language. So elegant! So natural! Like it was made to fit my brain. It's a pity I ended up working professionally with entirely different stuff.

_old_dude_|3 months ago

Yes, the version in Java is clearly less elegant. Java has map+lambda and compareTo (<=>) but no tuple assignemnt and no splat.

    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);
      }
    }

tombert|3 months ago

There’s no accounting for taste but I have never really seen why people consider Ruby so elegant.

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 :).

AdieuToLogic|3 months ago

> 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.

Scala would like to have a terse say about this... :-)

chamomeal|3 months ago

I don’t know any ruby but I dabbled in elixir and I gotta ask: why do you prefer parenthesis-less function calls?

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?

joemasilotti|3 months ago

> The author's AppVersion class is so nicely done, it's nuts how succinct eg the compare implementation is.

Why thank you! :D

BiteCode_dev|3 months ago

Modern Python is quite close to the ruby version:

    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.

__jonas|3 months ago

I don't really know Ruby, what is the to_s doing in

       parts = version_string.to_s.split(”.”).map(&:to_i)
Is it to_string? Isn't version_string already a string?

zumu|3 months ago

It allocates 2 collections for every compare call and obfuscates the comparison logic. Personally I find that extremely inelegant. Different strokes I suppose.

shevy-java|3 months ago

> But had forgotten how core Ruby is just an excellent programming language, regardless of what I think of the Rails ecosystem.

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.

kelvinjps10|3 months ago

I have always found python very succint, is ruby even more?

zahlman|3 months ago

> modern (to me) Ruby's lambda syntax (in the .map call)

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.

lloeki|3 months ago

> it’s built into Ruby!

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...

dragonwriter|3 months ago

Nitpicking your nitpick, but Ruby’s standard library has three components:

* 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/

jes5199|3 months ago

I just remembered, in those days, there was an alias called `ubygems` so you could pass `-rubygems` (ie, `-r` with `ubygems` as the argument) on the command line as if it was a first-class feature

it's so typical of ruby culture "haha, what if I do this silly thing" and then that gets shipped to production

jaredcwhite|3 months ago

Ruby is an awesome language. The first few 3.x releases brought incredible modern advancements, including pattern matching which I totally adore.

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.

kazinator|3 months ago

TXR Lisp:

  $ 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) :)))

kazinator|3 months ago

These objects can be inserted into a hash table, and are keyed by their semantic value, not the string:

  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

Bergrebell|3 months ago

Going back to rails after all this JS years was the best decision we’ve ever made. Current state of rails is lit!

websitescenes|3 months ago

Damn, I miss ruby and particularly Rails sooo much. I'm stuck rebuilding wheels in node currently :)

c-hendricks|3 months ago

title is actually "Ruby already solved my problem"

throwaway81523|3 months ago

Thanks, that helped. My unspoken question when I saw the title was "does that mean you now have two problems?".

aldousd666|3 months ago

I discovered this a few years ago when someone who didn't understand what semver is was trying to do a rails version upgrade for us. They were practically throwing stuff when I got there and explained that lexicographical comparison of the strings would not work. I was about to write my own class for it, but then I thought that since Bundler knew how to resolve deps we should see what it uses. The rest is history!

psadauskas|3 months ago

I use it quite a bit when I have to monkeypatch a gem to backport a fix while I wait for a release:

    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.

shevy-java|3 months ago

> I discovered this a few years ago

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 ... :>

saghm|3 months ago

> They were practically throwing stuff when I got there and explained that lexicographical comparison of the strings would not work.

Versions numbers can go to 10!?!

stefan_|3 months ago

Aaand 10 years later you just learned to compare versions by equality instead of being impossibly clever.

another_twist|3 months ago

I wish I was in the Ruby camp. Unfortunately the learning curve is quite high, you kinda have to know what you are doing from a language standpoint. I am more of a Java nerd myself and for me the cost of switching to any other language is simply not justified. But every now and then, I do get fomo.

PufPufPuf|3 months ago

Today we're praising Ruby for... having a version parsing utility in the standard library? Really? Many languages like Python, C#, even PHP have it too.

jbverschoor|3 months ago

Just not the same.

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

parentheses|3 months ago

I think this article is funny. Python's STL is way more useful and contains myriad useful things that Ruby lacks out of the box.

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!

zahlman|3 months ago

"STL" is not an abbreviation for "standard library". It doesn't even correctly refer to the C++ standard library; that's a misnomer. There was only one STL, and it was third-party; saying "Python's STL" makes barely any more sense than saying "Python's Boost".

https://en.wikipedia.org/wiki/Standard_Template_Library

matusp|3 months ago

If I would get a cent every time I solved a difficult problem in a project by pulling out difflib and shocking the team with it, I would have two cents. It's not a lot, but it's amusing that it happened two times already.