Ruby's Array: a Swiss Army Knife?
Tags: ruby, oop, metaprogramming, antipatterns
Published: 2019-12-15 03:39:01

---
title: Ruby's Array: a Swiss Army Knife?
published: true
description: Analyzing the collection types that are contained inside Array, and a crazy idea using metaprogramming to constraint the protocol depending on how we want to use the array.
tags: ruby, oop, metaprogramming, antipatterns
---

## Intro

In one of the lectures about OOP I usually give at university we go through design patterns and antipatterns. When we first studied the [Swiss Army Knife antipattern](https://sourcemaking.com/antipatterns/swiss-army-knife), the students asked me for an example and I didn't hesitate for a second. I immediately thought of the `Array` class in Ruby!

## Problem

On a very questionable design decision, Ruby has an `Array` class that can be used as any type of collection. Want a fixed-size collection? use `Array`. Want a queue or a stack? use `Array`! Want a simple ordered collection? Use `Array`!

Next there's a list of all the methods instances of `Array` understand:

```
> Array.instance_methods(false)
=> [:transpose, :fill, :assoc, :rassoc, :uniq, :uniq!, :compact, :compact!, :to_h, :flatten, :flatten!, :shuffle!, :include?, :permut
ation, :combination, :sample, :repeated_combination, :shuffle, :product, :bsearch, :bsearch_index, :repeated_permutation, :map!, :&,
:*, :+, :-, :sort, :count, :find_index, :select, :reject, :collect, :map, :pack, :first, :any?, :reverse_each, :zip, :take, :take_whi
le, :drop, :drop_while, :cycle, :insert, :|, :index, :rindex, :replace, :clear, :<=>, :<<, :==, :[], :[]=, :reverse, :empty?, :eql?,
:concat, :reverse!, :inspect, :delete, :length, :size, :each, :slice, :slice!, :to_ary, :to_a, :to_s, :dig, :hash, :at, :fetch, :last
, :push, :pop, :shift, :frozen?, :unshift, :each_index, :join, :rotate, :rotate!, :sort_by!, :collect!, :sort!, :select!, :keep_if, :
values_at, :delete_at, :delete_if, :reject!]
```

And I'm being a nice fella and didn't include the extension methods added in ActiveSupport (remember [#forty_two](https://apidock.com/rails/Array/forty_two)?) ;-)

`Array`'s multipurpose use goes against several object-oriented principles. There are no single responsibilities and there are groups of messages only valid for certain contexts. The class loses its essence just to become an object that tries to make everyone happy. The different collection types are not reified at all. Furthermore, if I want to use a stack in my program, I would expect a `Stack` class with an essential protocol of `push`, `pop`, `top`, `empty?` and nothing else. I'm definitely sure I wouldn't want to retrieve the third element of the stack, for instance. Or to add an element to the bottom.

### Collection types

The idea of specific collection types is not new: [Smalltalk-80](https://www.cs.virginia.edu/~evans/cs655/readings/smalltalk.html) specification includes (as of today) the best, in my opinion, collection hierarchy with clearly defined collection types: `Array`, `Set`, `Bag`, `OrderedCollection`, `SortedCollection` and so on. Even though this collection hierarchy has some design problems, it allows the developers to choose a collection type that fits the domain they are working with. You can use those collections in pretty much any Smalltalk dialect, such as [Squeak](https://squeak.org/), [Cuis](http://www.cuis-smalltalk.org/) or [Pharo](http://pharo.org/).

## Possible Solutions

### Don't use Array for everything... have you heard about the Set class?

Ruby has a [`Set` class](https://ruby-doc.org/stdlib-2.6.5/libdoc/set/rdoc/Set.html)! Unfortunately, I rarely see it on Ruby programs, maybe because it's easier to create an `Array`. Just type `require 'set'` and you'll be set (pun intended) to go. Now that you know about it, it's time to use it!

### Metaprogramming to make Arrays more specific

Let's say I want to enforce a list to be used as a fixed size collection. What I can do, given I don't want to implement a brand new collection class, is to "select" just the methods we want, depending on the type of collection we need.

So I want to change default `Array`’s behavior (and the way I want to use it) from this:

```
> my_kind_of_array = [1,2,3]
=> [1, 2, 3]
> my_kind_of_array << 4
=> [1, 2, 3, 4]
```

to this:

```
> my_true_array = [1,2,3].as_array
=> [1, 2, 3]
> my_true_array << 4   
NoMethodError: undefined method `<<' for [1, 2, 3]:Array
```

This would indicate that I want to use my list as a fixed size collection (that's the purpose of the `as_array` message) and therefore, every method to add/remove elements should not be understood on that object. That way I can have (in a dynamic way and still using native `Array` methods) the exact essential protocol I need.

Here’s a way to implement this:

```ruby
class Array
  def as_array
    instance_eval do
      undef :<< # and do the same for other methods that add/remove elements
    end

    self
  end
end
```

By using `instance_eval` I'm able to change that particular instance I'm using, and `undef` makes sure that the method is completely removed, and for the end-user, it seems the message never existed at all.

I can even change the semantics of some of the messages. For instance, `<<` on a queue could mean "enqueue", and on a stack could mean "push". Everything you want could happen inside an `instance_eval` block: removing methods, adding new ones, and even changing some semantics. Here’s an example of a change in the semantics of `<<` to have an array working as a set.

```ruby
class Array
  def as_set
    instance_eval do
      undef :[], :[]=, :at, :first # and more

      def <<(object)
        super(object) unless include?(object)
      end
    end

    self
  end
end
```

This is just an experiment, the implementation is not complete but you might get an idea of the final goal.

## Conclusions

* We still have some antipatterns living in our daily languages and tools. But sometimes, as in Ruby, we have the power to modify the language and/or extend it to suit our needs. This is an option we don't usually consider (maybe for historical reasons, and most people say it's "bad" without strong arguments) but it's perfectly valid.
* Metaprogramming is very helpful in this case, not to add new behavior but to "turn" the swiss army knife in a single, one-purpose tool.
* If collection classes only have essential behavior, developers get more "educated" and they are "forced" to think which kind of collection they want and use it consistently. This would prevent some unexpected hacks to happen.