DEV Community

Ary Borenszweig
Ary Borenszweig

Posted on • Edited on

Why I love Ruby: string representation

String representation

In Ruby every object responds to to_s with a good default. There's also inspect which usually reveals the internal structure of an object.

For example:

class Point
  def initialize(x, y)
    @x = x
    @y = y
  end
end

point = Point.new(1, 2)
point.to_s # => #<Point:0x00007fab148d55d8>
point.inspect # => #<Point:0x00007fab148d55d8 @x=1, @y=2>
Enter fullscreen mode Exit fullscreen mode

Okay, maybe to_s isn't that useful and inspect is much more useful, but it's nice that they are there.

The nice thing about inspect is that they also takes potential cycles into account:

class Person
  def initialize(name)
    @name = name
    @sibilings = []
  end

  def add_sibiling(person)
    @sibilings << person
  end
end

ary = Person.new("Ary")
gabriel = Person.new("Gabriel")
ary.add_sibiling(gabriel)
gabriel.add_sibiling(ary)

ary.inspect # => #<Person:0x00007fe670851160 @name="Ary", @sibilings=[#<Person:0x00007fe6708510c0 @name="Gabriel", @sibilings=[#<Person:0x00007fe670851160 ...>]>]>
gabriel # => #<Person:0x00007fe6708510c0 @name="Gabriel", @sibilings=[#<Person:0x00007fe670851160 @name="Ary", @sibilings=[#<Person:0x00007fe6708510c0 ...>]>]>
Enter fullscreen mode Exit fullscreen mode

Not only it doesn't crash: it also shows the object ID of objects so you can know that when there's a cycle, what that object is.

Similar code works fine in Crystal too:

class Person
  def initialize(@name : String)
    @sibilings = [] of Person
  end

  def add_sibiling(person)
    @sibilings << person
  end
end

ary = Person.new("Ary")
gabriel = Person.new("Gabriel")
ary.add_sibiling(gabriel)
gabriel.add_sibiling(ary)

ary.inspect # => #<Person:0x107d3aea0 @name="Ary", @sibilings=[#<Person:0x107d3ae60 @name="Gabriel", @sibilings=[#<Person:0x107d3aea0 ...>]>]>
gabriel.inspect # => #<Person:0x107d3ae60 @name="Gabriel", @sibilings=[#<Person:0x107d3aea0 @name="Ary", @sibilings=[#<Person:0x107d3ae60 ...>]>]>
Enter fullscreen mode Exit fullscreen mode

The above output is a bit hard to read... so Ruby and Crystal allow you to pretty print objects, every type of object, right out of the box. Here's Ruby with the above value:

require "pp"
pp ary
Enter fullscreen mode Exit fullscreen mode

Output:

#<Person:0x00007ffad4076fd0
 @name="Ary",
 @sibilings=
  [#<Person:0x00007ffad404b8a8
    @name="Gabriel",
    @sibilings=[#<Person:0x00007ffad4076fd0 ...>]>]>
Enter fullscreen mode Exit fullscreen mode

Here's Crystal:

pp ary
Enter fullscreen mode Exit fullscreen mode

Output:

#<Person:0x101418ea0
 @name="Ary",
 @sibilings=
  [#<Person:0x101418e60
    @name="Gabriel",
    @sibilings=[#<Person:0x101418ea0 ...>]>]>
Enter fullscreen mode Exit fullscreen mode

In other languages this ability to easily inspect objects out of the box is not present, or is not great. At least in some languages all objects can be converted to a string, even if the output isn't very useful. But some languages doesn't do that... and I'll mention Haskell now, maybe because I'm using it at work, but I think this is also true in Rust.

In Haskell you can't turn any value into a string by default. The type has to implement the Show typeclass. In practice this means that if we have the Point type I mentioned above:

❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Prelude> data Point = Point { x :: Int, y :: Int }
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p1

<interactive>:8:1: error:
    • No instance for (Show Point) arising from a use of ‘print’
    • In a stmt of an interactive GHCi command: print it
Enter fullscreen mode Exit fullscreen mode

You can't see what's p1. You again have to put deriving (Show) at the end of the declaration, or define a custom show function for the Show typeclass.

This might not sound like a lot to do, but when you are debugging code and you can't inspect objects and then you have to stop what you are doing and what you were thinking about, to go and open a file and add deriving (Show) in a lot of places just to see what's going on, it's not fun. It's less fun when those types aren't in your control and it's harder to add a Show for them. At the end of this process you end up thinking "should I leave these deriving (Show) or should I remove them now that I'm done with them", and it's where my question "why aren't these derived by default" comes to mind.

This is also when Ruby's goal, "developer happiness", comes to my mind. Doing all of the above isn't fun. In Ruby we don't have to do that, Ruby took care of that and we can just have fun solving more interesting problems.

That said, once you add deriving (Show), Haskell works fine, and in general show is very well implemented for "standard library" types:

❯ ghci
GHCi, version 8.10.7: https://www.haskell.org/ghc/  :? for help
Prelude> data Point = Point { x :: Int, y :: Int } deriving (Show)
Prelude> p1 = Point { x = 1, y = 2 }
Prelude> p1
Point {x = 1, y = 2}
Prelude> [p1]
[Point {x = 1, y = 2}]
Prelude> ["hello", "world"]
["hello","world"]
Enter fullscreen mode Exit fullscreen mode

Another language as an example: Go

Let's take a look at another language. I randomly chose Go because it's a relatively modern language, very popular, and there's a playground to try things out.

Here's an example from Go's tour about arrays:

package main

import "fmt"

func main() {
    var a [2]string
    a[0] = "Hello"
    a[1] = "World"
    fmt.Println(a[0], a[1])
    fmt.Println(a)

    primes := [6]int{2, 3, 5, 7, 11, 13}
    fmt.Println(primes)
}
Enter fullscreen mode Exit fullscreen mode

This is the output:

Hello World
[Hello World]
[2 3 5 7 11 13]
Enter fullscreen mode Exit fullscreen mode

Let's compare it to the output of Ruby or Crystal. I'm using p here which invokes inspect on objects.

a = ["Hello", "World"]
p a[0], a[1]
p a

primes = [2, 3, 5, 7, 11, 13]
p primes
Enter fullscreen mode Exit fullscreen mode

Here's the output:

"Hello"
"World"
["Hello", "World"]
[2, 3, 5, 7, 11, 13]
Enter fullscreen mode Exit fullscreen mode

The first thing to note is that Ruby and Crystal put quotes around strings (well, this isn't the case if you call to_s on a string, only inspect.) Then note that array contents use inspect. This makes it possible to know that the first array has two strings. In Go it's not clear: are there two elements, "Hello" and "World", or is it just one string "Hello World"?

Another nice thing is that you can copy that array output from Ruby and Crystal, paste it into a program and it will work. This isn't generally true, but it works really well for primitive types, arrays and hashes, which are used a lot! In Go this isn't true. There aren't even commas! I don't know why.

Now, I'm sure there's a way to show arrays or strings in Go in a better way. For example in Java you can use Arrays.toString. But not having that as a default adds friction. It's not the most intuitive thing you would expect to happen.

Another thing is consistency and uniformity. In Go there's the Stringer interface that defines the String() function to turn objects to a string. So what happens if we call String() on an array?

package main

import "fmt"

func main() {
    a := [2]string{"Hello", "World"}
    fmt.Println(a.String())
}
Enter fullscreen mode Exit fullscreen mode

We get a compile error:

./prog.go:7:15: a.String undefined (type [2]string has no field or method String)
Enter fullscreen mode Exit fullscreen mode

So it seems only fmt knows how to turn arrays into strings, and we have to rely on this package for that, but for other types we probably should use String().

It's those things that rarely exist in Ruby that I really appreciate. When something works in one way in Ruby you understand it and think "well, I guess this also works for these other types, or in these contexts" and that's almost always true. When that's not the case in a language, it's the moment you start building a collection of exceptions in your head, and when you start looking for answers in StackOverflow.

Coming up next...

I'll talk about making a language your own.

Top comments (0)