Hacker News new | ask | show | jobs
by nixpulvis 63 days ago
People should. I seriously miss using it at my day job. It's not for code where type systems make things a lot more stable, but it's great for scripting and quick things. Also ORMs in ruby are truly nice, and I haven't found anything as good anywhere else.

Generally speaking Ruby has the best APIs.

2 comments

Frameworks and packages, sure. I’m not sure I would agree with APIs.

ActiveAdmin is best in class, Rails is fantastic; but there’s a lot of insanity in the API for a language that “gets out of the way” and “just works”

Slice is my favorite example. (It’s been a bit since I’ve used it)

  [0].slice(0, 100) == [0]
  [].slice(0, 100) == … 
exception? Or nil? Why does it equal []?

For a “give me an array back that starts from a given, arbitrary index, and auto-handle truncation” not having that behavior continues to confuse me from an intuitive perspective. Yes, I understand the source of it, but why?

Because [] is an array with nothing in it, and [0] is an array with something in it.

So saying “give me the array containing the first 100 elements of this array with one element” would obviously give you the array with one element back.

Saying “give me the array containing the first 100 elements of this array with zero elements” would follow that it just gives the empty array back.

On top of that, because ruby is historically duck-typed, having something always return an array or an error makes sense, why return nil when there’s a logical explanation for defined behavior? Ditto for throwing an error.

Seems thoughtfully intuitive to me.

Yeah, returning an empty array is pretty much exactly what I would expect given the first example. It would be a lot weirder to me if you were allowed to give an end index past the last element only if the array happened to be non-empty.
Sorry, I mis-spoke earlier, this is what I should have shared:

  [].slice(5, 100)
^-- *THIS* either returns nil or throws an exception.

Edit: Longer example:

  puts "[1, 2, 3].slice(1, 100) -> #{[1, 2, 3].slice(1, 100).to_s}"
  puts "[1, 2, 3].slice(3, 100) -> #{[1, 2, 3].slice(3, 100).to_s}"
  puts "[1, 2, 3].slice(4, 100) -> #{[1, 2, 3].slice(4, 100).to_s}"
Yields:

  [1, 2, 3].slice(1, 100) -> [2, 3]
  [1, 2, 3].slice(3, 100) -> []
  [1, 2, 3].slice(4, 100) -> 
So, there is a behavior difference between "array a little too short" and "array slightly more too short" that creates unexpected behavior.

That's not a big surprise in a tiny example like this; but if you expand this out into a larger code base, where you're just being an array and you want the 100 through 110th values for whatever reason - say it's a csv. Suddenly you're having to consider both the nil case and the empty array case; but then why are they different?

Interesting! From playing around with it, seems like if the start index is exactly the same as the length, it returns empty array, but if it's further than that it returns nil. That's certainly not something I would have been able to predict, so I'd also be curious if anyone happens to know the explanation for it. My instinct is that it does seem like the type of edge case that might come up with a way to implement it tersely, but that's not a particularly good reason to leak that in the form of user-facing behavior, so hopefully there's a better explanation.

Some additional things I discovered when trying to figure out why it might work like that:

    * the behavior also seems consistent whether using `array.slice(a, b)` or `array[a..b]`
    * `array[array.length]` and `array[array.length + 1]` both return nil
the docs say... if index is out of range return nil. the edge case is that if you specify the exact end index of the array and want a slice of that index to 100 it will return an empty array. if you go out of bounds it informs you that you are out of bounds with nil. not sure it's the best api but probably is mimicking some C api somewhere as a lot of ruby does that. that said it will never error on this alone but it will almost certainly error if you chain it with something not expecting nil.

The easiest way to get around that if you are not carefully using the ranges would be to do `Array(array.slice(a, b))` as that will guarantee an array even if it's invalid. you could override slice if you really wanted to but that would be a performance penalty if you are doing it often.

looked into it more and the docs say that an index out of bounds will return nil. also says if offset == size and length >= 0 it will return an empty array.

``` If offset == self.size and size >= 0, returns a new empty array.

If size is negative, returns nil. ```

either way if you are doing stuff with arrays and not checking bounds you can throw an `Array(some_array.slice(x, x+100))` and it will always behave.

Sure, the docs seem to be accurate, but that only explains "what will this do?", not "why is this what it will do?" It's not what I expect most people would come up with if they designed this API, so I have to wonder why they didn't pick something more intuitive
Especially because in ruby

[0, nil, nil, nil, …x100, nil] is the same as [0] in terms of access.

In both cases, trying to access the 100th element (e.g. [0][100]) will give nil.

because it's meant to be a more functional language. if slicing an array out of bounds threw an error it would be java.

[].slice(0, 100).each do |x| puts x end

that shouldn't be an error and it seems to be the principle of least surprise imo.

Sorry, I mis-spoke earlier, this is what I should have shared:

  [].slice(5, 100)
^-- *THIS* either returns nil or throws an exception.

( I made the other comment like this longer, please use that one for context )

I actually think types are an anti pattern. I’ve seen more code with type escape hatches than bugs in Ruby. The truth is if you follow TDD and good coding patterns the bugs in a dynamic environment are unlikely to show up.