6 ms·
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
by skrebbel 1y 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.
- vault 1y agoLike 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.
- js2 1y agoChallenge 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}"
- alberth 1y agoNot knowing python, I find the data classes example extremely readable. More so than Ruby example.
- disgruntledphd2 1y agoI write mostly Python these days, but agree with op. The comparables implementation in Ruby seems much nicer to me (maybe because I'm less familiar with it).
- js2 1y agoIt's virtually the same in Python if you wrote it explicitly: def <=>(other) [major, minor, patch] <=> [other.major, other.minor, other.patch] end vs: def __lt__(self, other): return (self.major, self.minor, self.patch) < (other.major, other.minor, other.patch) Then use the `total_ordering` decorator to provide the remaining rich comparison methods. That said, it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct and what I did above is a slight fib: you still may need to add __eq__() as well.
- zahlman 1y ago> Then use the `total_ordering` decorator to provide the remaining rich comparison methods. While we're here, worth highlighting `cmp_to_key` as well for `sorted` etc. calls. > it's a little annoying Python didn't keep __cmp__ around since there's no direct replacement that's just as succinct The rationale offered at the time (https://docs.python.org/3/whatsnew/3.0.html https://docs.python.org/3/whatsnew/3.0.html) was admittedly weak, but at least this way there isn't confusion over what happens if you try to use both ways (because one of them just isn't a way any more).
- disgruntledphd2 1y agoI know, but the ability to use symbols to define the comparator is super, super cool, as opposed to the horrendously ugly lt dunder method.
- dlisboa 1y agoRuby 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/version.rb#L360-L406 https://github.com/ruby/ruby/blob/master/lib/rubygems/versio...
- jimbokun 1y agoWow I wonder why it's so verbose. Performance optimizations? Seems like this wouldn't be called often enough to show up in any performance profiling exercise.
- dlisboa 1y agoRuby is very slow so you gotta squeeze everything you can everywhere. Even a seemingly simple method will have to be scrutinized so that overall performance isn't impacted. It's death by a thousand cuts. See the commit that made it complex: https://github.com/ruby/ruby/commit/9b49ba5a68486e42afd83db49a48edf2bfc00105 https://github.com/ruby/ruby/commit/9b49ba5a68486e42afd83db4... It claims 20-50% speedups in some cases. There's churn that comes with that. Ruby will have code that is ever changing to gain 5%, 10% performance every now and then. You gotta put that on balance: in a language like Go this method would've been ugly from the start but no one would've needed to touch it in 100 years.
- jfabre 1y agoThere are plenty of businesses that have under 10k users and can live perfectly well with http requests around 500-1000 ms. When there are performance issues, 95% of the times they come from the database, not the language.
- __jonas 1y agoI 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?
- jimbokun 1y agoYes, to_s returns the string representation of an object. 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.
- dlisboa 1y agoRuby is a dynamic language, `version_string` can be anything. The author uses `to_s` to coerce it into a string. There are problems with that: if I pass in an array it'll coerce into `"[1,2,3]".split(".").map(&:to_i)`, which makes no sense.
- shevy-java 1y agoOne could do a conversion e. g. if x.is_a? Array x = x.first Or something like that. Could be one line too: x = x.first if x.is_a? Array
- dlisboa 1y agoMost times it's better to just accept the dynamic nature of the language rather than do this kind of runtime type checking. You'd have to do this `.is_a?` dance for every type to have it be reliable. Even if you implement an "interface" (duck typing) with `respond_to?(:to_app_version)` you still can't be sure that the return type of this `:to_app_version` is actually a string you can call `split()` on.
- metalliqaz 1y agoit allows you to initialize an AppVersion with an other AppVersion object
- shevy-java 1y 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.
- ajoski9 1y agoIs that true. Feels like new Ruby versions and great updates are being churned out relatively fast these days.
- joemasilotti 1y ago> The author's AppVersion class is so nicely done, it's nuts how succinct eg the compare implementation is. Why thank you! :D
- _old_dude_ 1y agoYes, 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); } }
- spullara 1y agoAlso the copyOf isn't really the same as being able to || things since it just happens both copyOf default is 0 and in this case it is also 0 (i.e. what if it was -1 to indicate there was no version).
- kelvinjps10 1y agoI have always found python very succint, is ruby even more?
- zahlman 1y 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/version.py 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.
- zumu 1y agoIt allocates 2 collections for every compare call and obfuscates the comparison logic. Personally I find that extremely inelegant. Different strokes I suppose.
- skrebbel 1y agoObfuscates how?
- AdieuToLogic 1y 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... :-)
- pxc 1y agoScala also draws on many other sources, but syntactically it clearly takes inspiration from Ruby in a lot of nice ways!
- skrebbel 1y agoFair! I was underwhelmed by a Python port in a sibling comment but I can imagine Scala could come close indeed! I mean it’d take a day to compile but it’d look pretty!
- chamomeal 1y agoI 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?
- tombert 1y agoYeah, I don’t really understand the hatred for parentheses. Lisp famously has parentheses everywhere, and it looks a bit noisy at first but the precedence and scope is never ambiguous and with Vim you can always do % to find the match.
- chamomeal 1y agoI would even say that the parens are a huge benefit of lisp. I guess that’s obvious though cause parens are like half the syntax. But once you get used to it: - you don’t really notice the parens anymore (definitely not the trailing ones anyway) - you get the advantages of structural editing. So many nice shortcuts for moving stuff around. After working in clojure, typing typescript feels so clunky. How do you focus the next argument? Jump to the enclosing function scope? Yank the body of the closure? You just have to actual navigate to those things and select them, so uncouth!!
- skrebbel 1y ago> why do you prefer parenthesis-less function calls? 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.
- asa400 1y ago> 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. Huh? [major, minor, patch] = version_string |> String.split(".") |> Enum.map(&String.to_integer/1) I don't know that I'd call this "substantially more" of anything.
- tombert 1y agoThere’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 :).
- BiteCode_dev 1y agoModern 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.