55 comments

[ 2.6 ms ] story [ 134 ms ] thread
Ruby's Range class has plenty of bugs to offer if you use it with anything except numbers, especially if you use any of its more "exotic" features.

I guess this isn't super constructive, but to me the whole thing smells of not just a lack of discipline, but a lack of _interest_ in correctness that seems to be endemic in the Ruby community.

We ended up writing our own `TimeRange` class to paper over the base Range bugs that show up if you use it with times.

One awkward takeaway from the experience is that I've come to believe that sometimes it _is_ worth unit testing other people's code, counter to the popular advice.

It's worth noting that there's a good chance many of these issues are well known by the Ruby community and many even get frustrated by them; *but*, so much code now relies on its behavior, predictably or otherwise, that they couldn't change it.

My pet frustration in Ruby is `.slice` behavior.

  > "abc".slice(0, 10)
  "abc"
  > "abc".slice(2, 10)
  "c"
  > "abc".slice(3, 10)
  ""
  > "abc".slice(4, 10)
  null
In Python, the last would also return "".

edit: updated the code to reflect a basic repl, without using puts or the 'or' statement, which handles the falsy behavior that "" and null both have in Python.

> My pet frustration in Ruby is `.slice` behavior.

I would argue that any input that doesn't have exactly one obviously correct output should result in an exception being raised. But Ruby often shies away from doing that. My interpretation is that they prefer to return _something_ rather than blowing up, so the show can go on.

I guess I sit at the opposite end of the philosophical spectrum from Ruby, so it's no wonder choices like this frustrate me. My personal philosophy is "explode early, explode often", because propagating invalid states only leads to future pain and suffering.

> [...] so much code now relies on its behavior, predictably or otherwise, that they couldn't change it.

I'm not denying this is a reason for keeping the questionably behaviour, but it's kind of funny because Ruby also introduces subtly backward-incompatible changes semi-regularly, including in "minor" releases. (Maybe that last bit is unfair, because I don't think that Ruby has ever claimed to follow semver.)

> My interpretation is that they prefer to return _something_ rather than blowing up, so the show can go on.

I think I saw this horror movie, but it was titled "PHP." :p

I agree it isn't entirely obvious at first glance why slice behaves the way it does, there is a pattern. If the first number in the range is a valid index into the string then you'll get something back, and if it isn't you can't get anything back so you have to get back a nil.

Coming from a C++ background my time in Ruby was weird and enlightening, it was less specified than C++, but pretty much everything is. I don't think it was underspecified, but a lot of the behavior was meant to be intuited and follows some kind of underlying pattern. If you miss the pattern you miss a lot of the functionality of that part of the language.

I agree that failing early and often is generally preferable. I don't see that Ruby doesn't do that, it just has more definitions for generic behaviors. Consider the amount of things in the range from the original post that produce type errors. Last time I checked both the C++ language and the C++ standard Library both had about twice as many pages in The Standard than all of Ruby in its standard Library have, yet there is still undefined and implementation to find behavior. So there are places where we get weird an unexpected Behavior that can result in bad State being passed around even in very formally specified places.

> If the first number in the range is a valid index into the string then you'll get something back, and if it isn't you can't get anything back so you have to get back a nil

But, 3 is not a valid index into a string of 3 characters. By your criteria, "abc"[3] correctly returns nil. Whereas "abc"[3,1] returns ""

The language designers really just did not think this through.

> but Ruby often shies away from doing that. My interpretation is that they prefer to return _something_ rather than blowing up, so the show can go on.

There are two (intertwined) explanations:

- In Ruby exceptions are costly to generate.

- Similar to Go, Ruby's stdlib design favours using return values for signalling, keeping exceptions for exceptional cases.

So in plain† Ruby there are a lot of places that use `nil` to signal that something's off, e.g `[1,2,3][4]` returns `nil` because it's an out of bounds access, or `"123".downcase!` returns `nil` because nothing has been downcased, or `__FILE__` returns `nil` when a file is `eval`'d.

So yes, you have to check for things like `nil` (or explicitly swallow with `&.` which is kind of a Maybe monad pattern) before doing stuff with values, just like you check for `if err == nil` in Go.

Running a type checker like `steep` helps a lot linting for these cases.

† Rails likes to generate exceptions, but Rails != the whole of Ruby.

I immediately see the logic in this API. When slicing, I look at indexes as being between elements or at the start (0) or end (length). This gives an in-bounds starting index between 0 and length, inclusive. So if the starting index is in bounds, you get a substring. If it's not, you get no result.

And your answer for Python is not quite correct: "" is falsy in Python, and both of the last two when translated to Python give "null".

Apologies, you're correct. I didn't consider the ramifications of printing to the screen with an or. I was trying to get rid of the lack of distinction between `puts null` and `puts ""` in Ruby.

And yes, I also understand the logic of the API; but if you're used to using slice to protect against random NPEs and out-of-bounds exceptions - which is something I do and am used to being able to trust in as a general pattern.

What I find weird here is the asymmetry: Ruby apparently allows the end index to be out of range, but not the start index. Contrast with, e.g., Rust's slice syntax, where both endpoints have to be in range, or else it will cause a panic.
> Ruby apparently allows the end index to be out of range, but not the start index.

What gives you that impression? "abc".slice(4, 10) is perfectly valid and accepted, assuming the code above is accurate.

Underneath the hood that's a C string, and the four points to the null terminator, so it's indexable. And that's why you get an empty string if you point exactly one past the end.

That's why if you put five or more in for the first index it fails to produce a result entirely. I think I might I preferred an exception or a failure code being returned, but I can't say the current design is truly awful.

> That's why if you put five or more in for the first index it fails to produce a result entirely.

Where does this come from? Are these discrepancies stemming from different Ruby implementations/versions behaving differently? "abc".slice(5, 10) returns the same value as "abc".slice(4, 10) [which, curiously, does not return the same value as the original comment] under MRI 2.6.1 that I had handy.

I believe I got it from the book Ruby under the microscope. Which looked at what is now a really old version of Ruby, and if they changed it to make a API more consistent that's probably good.
Fine, it's 'accepted' in the basic sense that it doesn't throw an immediate error. But it also doesn't return any useful string, as Python would. So you'd need an extra step to feed its output into anything expecting a non-null string.

The inconsistency here is that when you call "abc".slice(2, 10) and get "c", Ruby has implicitly truncated the range to return whatever characters are available, even though it can't go all the way to 10 because the string isn't long enough. But then when you call "abc".slice(4, 10), it doesn't just give you all available characters from index 4 (which would be an empty string), it gives you null instead.

> The inconsistency here

I don't see the inconsistency. slice on Array works the same way. Where is the inconsistency?

> (which would be an empty string)

What other aspect of Ruby would suggest that it is an empty string?

If what you are struggling to say is that different languages are different, then okay. "Japanese is unlike the English I know and therefore is inconsistent" would be a rather bizarre take, though.

> Where is the inconsistency?

Between what happens when the start index is greater than the length of the input, and what happens when the end index is greater than the length of the input. If the end index is greater than the length of the input, it returns a string (as long as the start index is not greater than the length of the input). But if the start index is greater than the length of the input, it does not return a string: it returns null, which is not a string.

My suggestion is that the behavior would have made more sense if it either returned a string in both cases (i.e., if it returned a string even if the start index is greater than the length of the input), or returned null in both cases (i.e., if it returned null whenever the end index is greater than the length of the input).

> Between what happens when the start index is greater than the length of the input, and what happens when the end index is greater than the length of the input.

Again, what makes that an inconsistency and not just a different language?

> My suggestion is that the behavior would have made more sense

On the basis of the start and end indices being equivalent. But are they? What attributes of the language should see us consider them to be?

(comment deleted)
If you see no reason to think that the two indices ought to have been equivalent, then there is no inconsistency that you might care about.

> What attributes of the language should see us consider them to be?

None.

Maybe they are equivalent, but looking at what other languages do is irrelevant in determining that. If there is nothing else in Ruby to suggest that they are equivalent, then perhaps they are not?

I'm not sure where "care" enters into the picture. It's a computer language. For what reason would emotions be assigned to it?

I ask for a range whose start is in bounds but end is out of bounds.

I ask for a range whose start is in bounds but whose end is out of bounds.

Why should those return two entirely different types?

but because of sane notion of falsiness in ruby, it's easy to both always have a string on unchecked inputs

  "abc".slice(4, 10) || ""

_and_ check those inputs in one go if needed.

  substr = "abc".slice(start, len)
  raise "wrong inputs" if substr.nil?
  
just that one feature (only nil and false are falsy) of ruby makes it positively stand out among all the languages that have "null" as concept
> sometimes it _is_ worth unit testing other people's code, counter to the popular advice

Relying on a library you don't have any tests for puts a lot of faith in it working. Also in it continuing to behave as you expected when you change the version.

People have weird ideas about other people's code though. Something being found on GitHub may mean it doesn't need it be reviewed or even glanced through before putting it into production while code written in house is obsessed over.

People in the software industry are just weird in general, it seems. I regularly see the same type of behaviour around blog posts by random authors drawing "My, what a fantastic idea, we must try this!", even when it clearly isn't a good idea, but anyone with ideas internally are automatically discounted.
>Relying on a library you don't have any tests for puts a lot of faith in it working.

To be fair Range is part of the language implementation. If you have no faith in the language implementation, why are you using it?

What are some of the bugs when using Range with Time?
> I've come to believe that sometimes it _is_ worth unit testing other people's code, counter to the popular advice.

The popular advice is that you shouldn't test implementation, only interface behaviour. Which means that you shouldn't explicitly test the usage of someone else's code within your code. Their code is just an implementation detail. If the implementation is faulty, testing of the interface should still reveal that faulty behaviour.

That doesn't necessarily mean you shouldn't ever test someone else's code (with their interfaces). But if someone else's code is not tested it is undefined, and relying on undefined behaviour is fraught with problems. It is unlikely you would want to use it in the first place, which, practically speaking, leaves little justification to invest in improving the situation.

Claiming "plenty of bugs" without stating any, doesn't really open the thread for constructive discussion, and frankly sounds like unnecessary FUD. Claiming that the ruby community doesn't care about correctness is borderline offensive, at least without evidence that reports are being ignored.
I've come to believe that sometimes it _is_ worth unit testing other people's code, counter to the popular advice.

I've never heard that advice

I basically always unit test the code I depend on -- to learn how it works!

---

libc in particular is worth unit testing. C has lots of "creative" APIs that are easy to misuse.

Like functions that return the same static buffer over and over again. I think the only way you find that is by running tests with ASAN enabled ...

libc functions also have so many error conditions, and they may differ between platforms.

Nice write-up, but no discussion of Ruby's Range class is complete without at least a mention of the venerable flip-flop!

Can you predict the output of the following?

    (1..20).each do |i|
      puts i if i.odd?..i.prime?
    end
This is one of the few features of Ruby I've never found a use for, and not fit lack of trying.

I thought I remembered Matz once saying it would be removed, But I could be wrong. Maybe someone uses it.

The flip-flop operator is very useful to extract continuous subsets, typically, sections of (multi-line) strings, where the dev defines the delimiters - think of the `=begin` and `=end` keywords.

I've personally never used for anything else than strings, but when I do, it's very useful.

Same as this?

  (1..20).each do |i|
    puts i if i.odd? || i.prime?
  end
edit - upon testing, I just realized the parent's code prints 10 & 16, my code does not, so not the same.
flipflop basically has a hidden boolean variable for state. Btw, while I HAD used it, i still have no idea what's the scope of that state and when it'd reset itself.
Relevant documentation link for the uninitiated: https://docs.ruby-lang.org/en/master/syntax/control_expressi...

“The form of the flip-flop is an expression that indicates when the flip-flop turns on, .. (or ...), then an expression that indicates when the flip-flop will turn off. While the flip-flop is on it will continue to evaluate to true, and false when off.”

[flagged]
[flagged]
The top comment gives a good reason. The Ruby community embraces broken software. Running Ruby software in production gives many devs a perpetual feeling of impending doom.
My time in the Ruby Community taught me about test driven development and a reliance on process that produced functioning and robust software. Things like continuous integration and careful specification through tests were promoted. If anything my most common problem with Ruby Community was too many different test suites to pick from. It seemed like there was a new one every week.

That said it was an non-typechecked interpreted scripting language and updates between versions could break things but if you had a process and a test suite those weren't practical issues.

What are some possible reasons that are experiencing the community differed so wildly?

A solo dev friend of mine is about to sign a 150k usd/year deal for his software written in JRuby. Initially installed on dozens of computers, it is to be run for months and years without intervention. So apparently there are ehhs and iffs and butts to all the off-hand dismissals. Maybe you have an elk here or there that would agree.
> is about to sign

What reservations does he have that has lead to holding himself back from already getting his ink all over that? Perhaps he is afraid Ruby won't stand up to what he is contractually promising?

[dead]
Is this your roundabout way of saying you don't know why he'd stall on signing what, at least as described, seems like a fantastic offer and are too afraid to ask?
No I'm just surprised that you are doubling down on this delusion. What does "I'm about to arrive" mean to you? Does it mean the person who is about to arrive hesitant?
"I'm about to arrive" means to me that you are already en route. "I'm about to sign" means to me that the pen hasn't yet hit the paper. In one case the process has already started, in the other process the has yet to commence. These are not comparable. "I'm about to leave" would be a more apt comparison, if we are to stay within proximity of your analogy.

"I'm about to leave" could, indeed, suggest hesitation. Why haven't you left already? –– Once the papers are able to be signed, the time is now. You can do so immediately. To wait means there is something holding you back.

What holds him back, we don't know. Apparently you are too embarrassed to ask or whatever that was that you were trying to say. But given the topic of discussion, presumably it is some kind of hesitation around the use of Ruby. Otherwise, where would the relevance of you telling us this story about your friend waiting to sign lie?

Okay, about to sign means the date is soon, and it never means anything else, and will never mean anything else in any situation to anyone else but a bad actor such as yourself. So you can stop making up stories. Hope that clears it up for you.
Oh, so not about to sign in any way, shape, or form. Rather, "about to maybe strike a deal (that is still likely to fall through, as many deal attempts do)"? Got it.

Hopefully next time you can keep your stories straight.

He is neither lucky, nor stiff.

(Your joke might have played better had you gotten the underscore in the right place)

Maybe he just doesn't like chunks in his bacon.
At the risk of incurring further downvotes:

- dynamic types

- confusing syntax

- idiosyncrasies in standard functions/libraries

- duck typing

- rails dogmas

Admittedly, my experience is limited (only 1 year of professional ruby dev), but I really did not enjoy my time with the language.

I think two of these are valid points, except I'm not sure what idiosyncrasies you mean, or what the rails dogmas are. If you are a lisp person, I can understand why the syntax irks you.

I think a strong criticism is (was) a lack of strong typing. These days editors can read your type annotations. They aren't as awesome as python typing, though.