Ruby through the lens of Go
Tags: ruby, go
Published: 2021-04-16 18:33:23

![Through The Lens](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1j61bs4s2rcz3pc0yc1f.png)

I've been an ardent Rubyist for over eight years now. I've dabbled in other languages, but at this point Ruby is the thing I'm most comfortable in.

Over time, languages and programming styles change and so do the people who use them. While the power and elegance of Ruby still makes it my go-to for writing things, a recent foray into Go is making me rethink some of the things I've been taking for granted.

## Why Go?

Go has been used at [Flipp](https://corp.flipp.com) for some time now, although not widely in my team. I wanted to use Go to create a command-line executable, something that Ruby unfortunately isn't capable of doing. (There are options, such as [ruby-packer](https://github.com/pmq20/ruby-packer), but it seems like a "heavy" solution and doesn't seem to fit the Ruby paradigm.)

Also, I was curious about Go as a whole, so I figured it was a great opportunity to learn more.

## Language Philosophies

Both Ruby and Go have extremely strong philosophies underpinning them. The guiding principle of Ruby is [programmer happiness](https://en.wikipedia.org/wiki/Ruby_(programming_language)#Philosophy). To achieve that aim, Ruby tries to be as flexible as possible, providing multiple ways of achieving the same thing in an effort to speak to each developer's preferred way of thinking. It supports lots of dynamism, allowing developers to "reopen" any class or module, or create whole new ways of interacting with code such as creating new [DSLs](https://en.wikipedia.org/wiki/Domain-specific_language).

I found it really interesting when reading up on Go that it *claims to have a similar philosophy*. In [their FAQ](https://golang.org/doc/faq#ancestors), it says "[Go was] designed by thinking about what programmers do and how to make programming, at least the kind of programming we do, more effective, which means more fun."

Having said that, it's pretty clear that Go is more of a [polemic](https://golang.org/doc/faq#creating_a_new_language) against systems languages such as C. When they say "more fun", they mainly mean *more fun than C*.

Go's real philosophy is a kind of extremist minimalism. The language is brutally strict about features that are and aren't allowed, keeping the feature set to an absolute minimum. The reasoning behind this is mainly that the language's raisons d'être are all about performance, speed, and simplicity. New features that might make the code itself slower or more complex are kiboshed.

As an example of the philosophical difference, here's how you would find an element inside an array in Ruby:

```ruby
arr = [1, 2, 3]
arr.index(2) # 1
```

And here's how you'd find it in Go:
```golang
arr := []int{1, 2, 3}
result := 0
for i, v := range(arr) {
  if (v == 2) {
    result = i
  }
}
```

Ruby, filtering out even numbers:
```ruby
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
filtered = arr.select(&:even?)
```

Go:
```golang
arr := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
filtered := int[]{}
for i, v := range(arr) {
  if (v % 2 == 0) {
    filtered = append(filtered, v)
  }
}
```

However, language design is only part of the story behind a language. Just as important is the *ecosystem*: the reusable code modules/packages, the people that manage them, which ones are "popular", what "best practices" are, etc.

One of the things I love the most about Ruby is that it tends to coalesce around one or two *really popular* libraries. [Rails](https://rubyonrails.org/) is the big one obviously, but over time you see libraries designed for a particular purpose "winning" over other things. This includes things like linting/code analysis ([Rubocop](https://github.com/rubocop/rubocop)), authentication ([Devise](https://github.com/heartcombo/devise)), testing ([RSpec](https://rspec.info/) and [Minitest](https://github.com/seattlerb/minitest)) and more. The emphasis is on making something good great rather than making a lot of different good things.

In Go, it seems like packages themselves are nowhere near as popular as the language, compared to what I see elsewhere. I haven't been in the ecosystem long, but it seems to be rare that articles about Go coalesce on a particular solution similar to Rails or something like [lodash](https://lodash.com/) or [express](https://expressjs.com/) in the JavaScript world. Instead, the main feeling when asked "How do I do X" in Go seems to be "do it yourself". The kind of thing that in Ruby would be derided as "plumbing" is "real code" in Go; the goal is to make you think about every single line you write.

Go's lack of generics make it difficult to make an all-purpose utility library to make these sorts of things easier, so maybe once they land in a year or two the rawness of the language could be mitigated. But for now, Go and Ruby feel like as far apart as they could be.

## What can we learn?

The strictness of Go means that there likely aren't too many things that we could import into it from Ruby and similar languages. And nor should we; one of the refrains of the Go philosophy is that languages should be *different* rather than similar, and Go is outspoken in its adherence to its own unique identity. 

However, I think that some of the surprising advantages I found in Go can be moved over to Ruby without much work other than a slight shift in mindset. (There are other amazing things Go provides that Ruby simply can't do, and in fact might start affecting the beautiful things about it - this post is not about all the nice things in Go but the things that we can do with our Ruby code to gain some of the same advantages.)

### Keep code and data separate

Here is how you define a method on a type in Go. (This example intentionally has a code smell, where I'm sending an e-mail from something that shouldn't be able to or care about it.)

```golang
type Product struct {
  Id string
}

func (product *Product) sendEmail(customerEmail string) {
  sendProductEmail(product.Id, customerEmail)
}

// ...

product = Product{Id: "some-product-id"}
product.sendEmail("me@example.com")
```

The usage of the method looks the way we expect it, but wait... something's missing from the function body. **Where's the `this`?**

The answer is that there isn't one. Not because the idea doesn't exist (it does - it's called a *receiver* and Ruby has the same idea) but because the syntax calls out a very important fact, which is that *methods and data do not need to live together in an object*.

To make this abundantly clear, let's realize that we made a stupid mistake and move the `sendEmail` function from the product to a top-level function:

```golang
func sendEmail(product *Product, customerEmail email) {
  sendProductEmail(product.Id, customerEmail)
}

// ...

product = Product{Id: "some-product-id"}
sendEmail(product, "me@example.com")
```

What's changed in our function body?

The answer: Absolutely nothing. The definition and usage changed, but the function body is identical. This is a very strong hint that *methods work on data*. And whether the methods and data "live together" in an object or not, the exact same thing is happening.

Keeping methods out of objects helps to reduce the urge to introduce side effects. And that means you can often model your functions as pure input/output, which has [a whole whack of advantages](https://www.deadcoderising.com/2017-06-13-why-pure-functions-4-benefits-to-embrace-2/).

As an example, take a Ruby implementation of the above.

```ruby
class Product
  attr_accessor :id
  def send_email(customer_email)
    send_product_email(@id, customer_email)
  end
end
```

Let's do the same refactor now:

```ruby
class Product
  attr_accessor :id
end

def send_email(product, customer_email)
  send_product_email(product.id, customer_email)
end
```

We've had to change our function body to accommodate this now.

So how can we achieve the same flexibility in Ruby? Simple. *Treat data as data and methods as methods.* Prefer static methods that work on data. This has the added advantage of enabling you to "pipe" data more easily without having to fall back to solutions I've always found odd like `tap` and `yield_self`.

```ruby
class Product
  attr_accessor :id
  def self.send_email(product, email)
    send_product_email(product.id, email)
  end
end

Product.send_email(my_product, 'me@example.com')
```

The functions stay in the class definition (Go doesn't have class functions, which IMO makes it a bit harder to organize) but are static and don't use or change any object state. 

For example, let's say we've decided to move this function to a new module which deals with all the e-mail sending. The function doesn't change at all any more - in fact, even the pattern of calling it stays the same. The only thing that changes is the class name.

```ruby
class Product
  attr_accessor :id
end

class EmailService
  def self.send_email(product, email)
    send_product_email(product.id, email)
  end
end

EmailService.send_email(my_product, 'me@example.com')
```

Obviously this won't work 100% of the time, but I've found this pattern to be much easier to work with.

## Use types and not hashes

Go is a statically typed language, in its own maverick way. It doesn't support inheritance, and mainly works through structs and interfaces (which is a great feature that Ruby can approximate, much less elegantly, via "duck-typing" and `respond_to?` calls). 

Ruby is dynamically typed, and in a typical Ruby project you'll see classes, OpenStructs (a class that basically makes a hash "pretend" to be an object by using dot-notation), and other solutions, but in many cases you're using hashes to store your data.

There are some real benefits to static typing, not the least that it makes IDE's much smarter at telling you when you're doing something wrong. Even better, types implicitly document what your data looks like much better than hashes.

Ruby is slowly starting to drift towards better type functionality, with first [Sorbet](https://sorbet.org/) and then [RBS](https://github.com/ruby/rbs). But you don't need any static typing tool in your own code to do things smarter.

It's not like Ruby doesn't already support structs! Check the difference here.

```ruby
# @param json [Hash]
def create_dashboard(json={})
  DashboardCreator.call(
    name: json[:name],
    graphs: json[:graphs],
    owner: json[:owner]
  )
end
```

In my opinion, it doesn't matter if `DashboardCreator.call` is defined with keyword arguments or an options hash. The fact that the input `json` is a hash means that your code has no idea if, for example, `:name` is a valid key into your JSON.

Hashes are maps. Maps are *key-value pairs*. They're meant for lookups. For some reason, Ruby has latched onto this idea that they should be used as options, keywords, or attributes. This is confusing and error-prone.

Now here's the same thing with structs.

```ruby
# @attr graphs [Array<Graph>]
# @attr name [String]
# @attr owner [String]
Dashboard = Struct.new(:graphs, :name, :owner)

# @param dashboard [Dashboard]
def create_dashboard(dashboard)
  DashboardCreator.call(dashboard)
end
```

Note how I've packaged up all the data into a single object which is "strongly typed", or as strong as Ruby can get for now. We (and our IDEs) now know absolutely what that object looks like.

Now there might be cases where all we've done here is push up the error cases where our parsed JSON doesn't match what we expect. For example, we might need a method that looks like this:

```ruby
# @param json [Hash]
# @return [Dashboard]
def parse_dashboard(json)
  raise "No graphs!" if json[:graphs].nil? || 
   !json[:graphs].respond_to?(:size)

  Dashboard.new(
    name: json[:name],
    graphs: json[:graphs],
    owner: json[:owner],
    keyword_init: true
  )
end
```

The thing is, we've moved the error into a method *whose only job is to parse JSON*. We can do further validations here. But importantly, we don't encode our expectations about JSON into all the downstream functions. Only one thing cares about JSON itself. Everything else cares about dashboards.

### Simplify, simplify

Ruby's dynamism allows for some truly concise and beautiful-looking code. But like always, you need to make sure your toolbox is using the right tools for the right situations.

When you're building a *framework*, using the more advanced metaprogramming features like `define_method` and `method_missing` can work well and provide a useful interface. But using it in your code can cause a world of pain.

Just because you *can* do something in Ruby doesn't mean you *must* do it. I've moved over to preferring *code generation* over *code reflection*. In other words, using generators to create the code you care about rather than deciding what your code should do at runtime.

For example, let's say you have a web parser that has to fetch a number of item fields by using different XPath or CSS selectors. We want to define a parent class that defines all these fields and child classes that separately specify how to fetch them for different websites. Here are a couple of different ways to achieve this.

First, how we want to use this code:

```ruby
Item = Struct.new(:name, :description, :price)

class Site1Parser < Parser

  def initialize(item)
    @item = item
  end

  def name(browser)
    browser.xpath("/some/path/to/name")
  end
end

parser = Site1Parser.new
parser.item.name # the value parsed from the site
```

Way number 1 uses `method_missing` to set attributes to an item:

```ruby
class Parser

  attr_accessor :item

  def parse
    browser = Browser.new
    Item.members.each { |f| self.send(f, browser) }
  end

  def method_missing(method, *args, **kwargs, &block)
    if item.respond_to?(method)
      item.send("#{method}=", block.call(args[0]))
      return
    end
    super
  end
end
```

Option 2 goes a bit more explicit by actually defining methods using `define_method`:

```ruby
class Parser
  attr_accessor :item

  def parse
    browser = Browser.new
    Item.members.each { |f| self.send(f, browser) }
  end

  Item.members.each do |field|
    define_method(field) do |browser|
      raise "Not implemented yet!"
    end
  end
end
```

Leaving alone the possible debugger confusion, when we humans are looking at either of these options, we need a lot of information in addition to the body of the method. What are valid values for `method` or `field`? What possible arguments could be passed? What does `self` mean at this point?

What if instead, we created a [generator](https://guides.rubyonrails.org/generators.html) which created code for you? Something that might look like this?

```ruby
class Parser

  attr_accessor :item

  def parse
    browser = Browser.new
    self.name(browser)
    self.description(browser)
    self.price(browser)
  end

  def name(browser)
    raise "Not implemented yet!"
  end

  def description(browser)
    raise "Not implemented yet!"
  end

  def price(browser)
    raise "Not implemented yet!"
  end
end
```

In this case, rather than writing all this code yourself, you rely on the generator to do it for you. Some benefits of this approach:

* The code is much easier to understand, because you can actually look at each method and see exactly what it calls, without having to step through it or check to see what `Item.members` returns.
* Your codebase becomes searchable, both by you and by code analysis tools. Your function calls are explicit and you can logically step through them without confusion.
* You can more easily override specific methods as necessary when they're already there for you to do so. This is the kind of thing object oriented design is really good at. And you can easily tell what methods you *have* to override.

Obviously you run into other problems on the framework side (when you add a field to `Item`, how do you make sure you can update the code without blowing away anything you've changed in it?) which might involve more advanced [abstract syntax tree](https://medium.com/flippengineering/using-rubocop-ast-to-transform-ruby-files-using-abstract-syntax-trees-3e352e9ac916) munging. But I'll leave this here as a thought experiment, nevertheless.

## Conclusion

Both Ruby and Go are amazing languages in their own way, and both provide numerous benefits in different directions. There are definitely things I wish I could change about Go, but the nice thing about Ruby is that I know I *can* change some things about it all on my own!