Hacker News new | ask | show | jobs
by nileshtrivedi 3099 days ago
Ruby may not be the cool thing in town it once was, but it remains my preferred language for prototyping ideas - purely because of its elegance and expressivity. It truly delivers on its fundamental goal of "developer happiness". A lot of ideas from Ruby have gone to other languages (CoffeeScript, Elixir) and many web frameworks are modelled after Rails.

Congrats and thanks to all the core devs! :-)

1 comments

See, expressive I totally get. Absolutely. But elegance? Don’t understand that one.

Ruby is a cluster of inelegant and over concise cludges in the name of expressivity. The fact that it was once most famous for one of the most inelegant hacks - monkey patching - as a core feature speaks to that.

So I totally understand that it’s quick, it’s expressive, and it’s easy to prototype in. Agreed. It’s lost a lot of it’s shine there now but it’s all still true.

But elegant? It’s concise, not elegant.

Disagree 1000%.

Just because you can write a monstrosity in Ruby doesn’t make the language itself inelegant.

Ruby, of the many languages I’ve used, has by far the highest potential for code being poetry. You can find solutions that are incredibly simple and understandable due to so much boilerplate being remove from the syntax. Likewise you can write code that’s impressively concise without being inscrutable — I’m continually astonished by how readable Ruby can be.

One exercise in particular sticks out in my mind. Many years back, I implemented the first few dozen Project Euler problems in Ruby. However, I imposed a limitation: each solution had to be under 72 characters (library inclusions are fine), a single logical line, and readable. The end result was surprising at how naturally it ended up working — the solutions ended up being little more than the high-level conceptual approach, translated literally to code. Solutions ended up looking like the following:

    (2 ** 1000).digits.sum
    (2..10_000).amicable_numbers.sum
    1901.to_year.upto(2001.to_year - 1).select {|d| d.mday + d.wday == 1 }.count
Sure, these depended upon injecting new methods into core classes, but who cares? For this problem it led toward incredibly straightforward and readable solutions for these problems. If this were library code I wouldn’t dream of doing it this way, but for application code — why not?

I have yet to see any other language with Ruby’s capacity for readable conciseness.

Are you familiar with Rust, Haskell, C# or JavaScript?

All of these allow creating new extension methods of existing types, albeit by three different methods.

I'm assuming that your "2001.to_year - 1" is "the date time for 2001 minus 1 day"

rust (traits)

    (pow::<BigInt>(2u8, 1000).digits().sum()
    (2..10_000).amicable_numbers().sum()
    1901.to_year().upto(2001.to_year() - 1.to_day()).days().iter().filter(|d| d.mday + d.wday == 1).count()

Haskell (type classes)

    sum . digits $ 2 ^ 1000
    sum $ amicable_numbers [2..10000]
    count $ filter (\d -> mday d + wday d == 1) [year 1901 .. year 2000 - day 1]

C# (extension methods)

    BigInteger.Pow(2, 1000).digits().sum()
    Enumerable.range(2,10000).amicable_numbers().sum()
    1901.ToYear().UpTo(2001.ToYear() - 1.ToDay()).Days().Where(d => d.mday + d.wday == 1).Count()

JavaScript (changing prototypes)

    bigInt(2).pow(1000).digits().sum()
    _.range(2, 10000).amicable_numbers().sum()
    1901.to_year().upto(2001.to_year().minus(1)).days().filter(d => d.mday + d.wday === 1).length()
Yes, I am familiar with all of them. :) Rust most of all, and that's another language I'm extremely happy with. The others to lesser extents, and Javascript is the only one of them where I'd say I dislike the language.
> Sure, these depended upon injecting new methods into core classes, but who cares?

I, as a developer reading your code 6 months later, care.

When something fails, it's nearly impossible which of the magic methods were injected by which magic library.

Not really. It's hard for your editor to do this statically, but there's heaps of good tooling for finding the method source and docs dynamically.

  pry> show-source SomeClass.class_method
  pry> show-source AnotherClass#instance_method
  # List an arbitrary object's class methods, instance methods, methods mixed in by included modules
  pry> cd some_object
  pry> ls
The runtime knows how to execute your program (it's not random) so everything you want to know is available.
Been doing Ruby full-time for 3+ years, bunch of experience before that.

    there's heaps of good tooling for finding the 
    method source and docs dynamically
I know, but this is a huge problem with a lot of code I see in the wild because this stuff is (ab)used so widely in Ruby-land.

If I have to run code just to see where `Foo.bar` is defined, that makes things... about an order of magnitude harder to understand. I mean, that's really tough thing to do in anything but a trivial codebase.

This is nearly 100% the fault of developers, not the language itself, but I would suspect that most Ruby developers spend a significant amount of time dealing with things like this because it's how the ecosystem rolls.

You realise you just validated my point, right? :)
I'm not sure you did.

People who don't use Ruby seem to think this is a huge thing that you run into constantly. It's not. People are for the most part pretty responsible with these things.

It’s very far from impossible. It is trivial to programmatically determine where a method was defined using Method#source_location.

Determining which method responds to which message is only possible in an instantiated runtime, but that’s true for any late-binding programming environment. Complaining about it tells us more about the complainer than about the language.

Rust's way of doing this is nice. It allows you to extend core types (e.g. add methods to integers), but in order to use the added methods you import the trait that adds them in the file you want to use them in (so you can always have a good idea of where they've come from)
It's almost as if you didn't even bother reading the sentence immediately following.

If you're writing a library doing things like this is a bad idea (except in very limited circumstances). And to what seems to be every non-Rubyist's surprise, virtually no libraries in practice actually do this sort of thing so there are never problems in practice.

The times I do this sort of thing in my own application code, I put the methods in a module and have a hook that raises an exception if the methods already exist when the module is included. This is the best of both worlds: I can add features that don't exist, but am informed immediately when my app launches if such a method has already been defined.

I think there's a lot about its expressiveness that's elegant. You can't pick one inelegant practice and define the entire language by that — nothing would pass that test.

But for example, Ruby's method-based control flow allows for sophisticated ideas to be expressed in a way that's clear but doesn't demand undue attention. A while back I needed to implement a common structure in Python for basically "repeat this operation with increasing sleep times until it succeeds" in the context of a particular library. Every option I could come up with was fairly inelegant, either requiring more attention than it deserved or being excessively oblique or just reading like a neural networks trained on Python, and none of the senior Pythonistas at my work could come up with anything much better. But I could clearly picture how to do it in Ruby, and it wouldn't have had any of those problems there. The extra expressiveness allows it to express the idea elegantly.

What would the elegant Ruby version look like, and what was inelegant about the Python version?

In Python I imagine you could just do something like this:

    from time import sleep

    sleep_increment = 1
    sleep_time = 0
    success = False
    while not success:
        sleep(sleep_time)
        success = try_thing()
        sleep_time += sleep_increment

    def backoff(wait = 5, exponent: 1.5)
      yield
    rescue BackoffError
      sleep(wait)
      wait = wait ** exponent
      retry
    end

    backoff { connect_to_a_thing }
This is trivially composable with any other control flow you might want to write in Ruby.

Personally I find the Python approach inelegant. Can it be composed? Why am I having to work with low-level looping constructs instead of higher level control flow constructs that map more closely to the task I’m actually trying to accomplish?

Same thing goes for any non-functional looping at this point. Why am I having to care about loop indices, incrementing counters, creating result arrays and inserting items into them, etc.? It’s (almost) 2018 and people are still writing low-level looping logic for the n-billionth time. Worse, people have to read it and parse it for the n-trillionth time to figure out which looping idiom is being used, instead of being able to see at a glance that something is being mapped, selected from, reduced, etc.

Nothing precludes a similar approach in Python:

    def backoff(wait=5, exponent=1.5):
        while True:
            yield
            sleep(wait)
            wait = wait ** exponent

    for backoff():
        connect_to_a_thing()
Here's the question: do you need exponential backoff in more than one place? Because if you don't, bundling all of that doesn't buy you anything and the original works just fine.

> This is trivially composable with any other control flow you might want to write in Ruby.

And it is actually trivially composable with other Python control flow, it's just a backoff iterator, you can drive it however you want, or compose it with other iterators (e.g. enumerate() to know which instance you're on, islice to stop after a certain number of tries, …)

Your solution is much cleaner to me, except (not really knowing Python) it looks like it might have a bug, in that the `while True` will never terminate.
It looks like the Ruby-specific elements here are (1) the 'retry' statement, and (2) the ability to yield a block twice (Python's equivalent `with` syntax explicitly prohibits re-yielding a block).

(1) might just be a matter of taste -- that 'retry', to me, makes me now have to step back and think "oh, I'm retrying something now? what exactly is being retried here?" Having it all wrapped up in a 'while' loop makes the intent explicit right from the get-go.

(2) I agree is nice. It would be great if you could do this:

    with backoff():
        connect_to_a_thing()
but you're not allowed to 'yield' twice inside a context manager. However you can get damn close (and fully composable) by wrapping the connection logic and the retry logic in functions:

    def connect_to_a_thing(url, access_token):
        ...

    def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
        success = False
        while not success:
            try:
                connector(*args, **kwargs)
            except BackoffError:
                sleep(wait)
                wait = wait ** exponent
            else:
                success = True

    backoff(connect_to_a_thing, url, access_token)
Or you could be even more explicit with recursion:

    def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
        try:
            connector(*args, **kwargs)
        except BackoffError:
            sleep(wait)
            backoff(connector, wait ** exponent, exponent, *args, **kwargs)
As for your point about loop indices, Python generally has wonderful support for not worrying about loop indices and counters. I'm also not sure how that applies in this case; you still need to increment the value of 'wait'. Then again...

    def exponentiate_forever(value, exponent):
        while True:
            yield value
            value = value ** exponent

    def backoff(connector, wait=5, exponent=1.5, *args, **kwargs):
        for wait in exponentiate_forever(wait, exponent):
            try:
                connector(*args, **kwargs)
            except BackoffError:
                sleep(wait)
            else:
                break
But please don't do that.
I don’t think you can call monkey patching inelegant or a hack. Monkey patching is the absence of extra rules - not allowing a method to be defined more than once - and the prescence of consistency - all methods can be redefined.

It’s certainly problematic when used! But the feature itself is elegant and the opposite of a hack. Disallowing monkey patching requires extra conditions and rules, not less.

Not to sidetrack the conversation, but will you be announcing anything regarding the GraalVM? It seems like there is something announced every year, would love to hear the progress from last year
Yeah I’ll try to post something to http://mail.openjdk.java.net/mailman/listinfo/graal-dev
Arguing that something isn't elegant is a lost cause.
What's an example of one of those kludges? "Ruby concise" at least looks elegant, whereas, say, "Perl concise" is hideous.
I’m continually impressed by how understandable golfed Ruby tends to be, especially when compared to Perl. Golfed Perl is completely inscrutable, whereas golfed Ruby is often times no less understandable than a longer version.
I disagree it even looks elegant. Short? Yes. Better than Perl? Sure, but even Perl fans wouldn’t call it a pretty language.