Why I love Ruby: a great standard library

asterite

Ary Borenszweig

Posted on February 21, 2022

Why I love Ruby: a great standard library

In many programs it's very common to want to read a file and process the contents line by line. Here's what you needed to do in Java some time ago (around the time I found out Ruby) to do that:

BufferedReader reader;
try {
  reader = new BufferedReader(new FileReader("/path/to/file.txt"));
  String line;
  while ((line = reader.readLine()) != null) {
    System.out.println(line);
    // read next line
    line = reader.readLine();
  }
} catch (IOException e) {
  e.printStackTrace();
} finally {
  reader.close()
}
Enter fullscreen mode Exit fullscreen mode

It works, but there's a lot to read and write, and the intent of the code is not immediately clear.

Here's how you do it in Go, based on this StackOverflow answer:

file, err := os.Open("/path/to/file.txt")
if err != nil {
    log.Fatal(err)
}
defer file.Close()

scanner := bufio.NewScanner(file)
// optionally, resize scanner's capacity for lines over 64K, see next example
for scanner.Scan() {
    fmt.Println(scanner.Text())
}

if err := scanner.Err(); err != nil {
    log.Fatal(err)
}
Enter fullscreen mode Exit fullscreen mode

Again, it works but it involves a few steps.

Here's how you do it in Rust, based on this StackOverflow answer:

let file = File::open("foo.txt")?;
let reader = BufReader::new(file);

for line in reader.lines() {
    println!("{}", line?);
}
Enter fullscreen mode Exit fullscreen mode

It's actually pretty concise, but it still involves a BufReader.

Here's how you do it in Ruby:

File.foreach("/path/to/file.txt") do |line|
  puts line
end
Enter fullscreen mode Exit fullscreen mode

So simple and concise! You want to read a file line by line, so you just tell Ruby to do that. No intermediate steps. No intermediate concepts like buffered readers.

Now, I know what you are thinking: in Java, Go, Rust, and probably any language, you can define a helper function to do that. Then the code ends up being as concise as Ruby.

But here's the thing: Ruby had this in the standard library right from the get-go. There's no need for all of us to write the same helper function in our projects.

And this was just about reading files line by line. When you start to use Ruby, you realize how many useful things are there in the standard library.

A comment about Java

Recent Java versions added helper functions to the standard library to read files line by line. Now it seems you can do this:

try (Stream<String> lines = Files.lines(file, Charset.defaultCharset())) {
  lines.forEachOrdered(System.out::println);
}
Enter fullscreen mode Exit fullscreen mode

Neat! I think this shows that eventually they realized that this was a common operation, and that it made sense to have this somewhere in the standard library. Ruby was a pioneer.

The hidden work Ruby does for us

So to read files line by line in Java, Go and Rust we needed to open a file and then use something else on top of that: a buffered reader in Java and Rust, and a bufio.Scanner in Go. What's going on?

Well, the operating system interface for reading things from a file is fill out this chunk of bytes with what you've got, and tell me how many bytes you put there. You can see this interface bubbling up into existing programming languages:

Then reading a file line by line has to be built on top of this interface.

However, doing these primitive calls involve system calls, and doing many system calls is very expensive! So one way to deal with this is to introduce an intermediary, a buffer: the buffer will read as much as possible from the file, and then we can process the buffer's contents in-memory. In this way we produce the least amount of system calls.

When you open a file in Go or Java, they are just handles to files. If you want to use them efficiently you need to put a buffer on top of them.

What did Ruby do? Remove the need to know this, and all this complexity, from users. When you open a file in Ruby, it internally has a buffer. It's already doing what you are most likely to be doing in other languages in a more complex way! Thank you, Ruby!

Why did Ruby do this? I don't know. One guess could be reducing complexity from users, which it tries to do all the time. Another guess could be: if to use files efficiently you'll have to use a buffer in 99% of the cases, why not make them buffered by default?

IO objects in Ruby are even more powerful!

But wait, there's more!

Let's say you need to read each line of a file... but this file is encoded in a non-UTF-8 encoding, for example EUC-JP.

It's very likely that in most languages you will need to introduce a new intermediary object to do the encoding, so you'll need a few more lines of code for this. It's probably fine.

In Ruby, you can directly set in which encoding you want to read or write things, like this:

File.foreach("/path/to/file.txt", encoding: "EUC-JP") do |line|
  puts line
end
Enter fullscreen mode Exit fullscreen mode

That's it!

The rest of the standard library...

This post was just about one thing: reading files line by line. But Ruby's standard library has things like these in many places. In the end you can get a lot out of the standard library by writing very little code, and also by not needing to use external dependencies.

The community

It's my impression that the Ruby community tried to do Ruby gems by mimicking the standard library way: simplify a user's life, provide helpful methods to do things by needing as few lines as possible.

Coming up next...

I'll be talking about a feature that's almost unique to Ruby: blocks!

💖 đŸ’Ș 🙅 đŸš©
asterite
Ary Borenszweig

Posted on February 21, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related