Don't mix Forwardable and ActiveSupport::Delegate
Published: 2021-12-02 11:47:13

A few days ago I released the [`disposable` gem](https://github.com/apotonick/disposable/commit/979dc5f4950881c913263340a12628ec1ea52566) minor line 0.6.0. The goal was to [remove unnecessary coupling](https://github.com/apotonick/disposable/commit/979dc5f4950881c913263340a12628ec1ea52566#diff-cbfbccff8b533b3c195c7058cf7245b5559ecf5f0a16b4e7b83390a2ade42be6) to the old `uber` gem that was once planned to hold a set of abstractions we need in all Trailblazer gems.

What I mainly worked on was removing `Uber::Delegate` and supersede it with Ruby's built-in `Forwardable`. We simply need to delegate a bunch of methods from one object to another, and `Forwardable` does just that.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yr8651xwcvieb9tx46w7.png)

So what the new code basically did was to extend the `Disposable::Twin` class and use `def_delegators` to create automatic delegations.

```ruby
class Disposable::Twin
  extend Forwardable # this used to be Uber::Delegate

  def_delegators :model, :title, :title=
  def_delegators :model, :email, :email=
```
Obviously, removing gem dependencies, even if they're your own, feels good, so I pushed this new gem after running tests in two projects and leaned back in my pricey office desk.

## Here comes another delegate!

A day later we received [bug reports](https://github.com/trailblazer/reform/issues/534) from several users. All of them were using [Reform](https://github.com/trailblazer/reform) form objects, the problem was thrown from `Reform::Form` classes they had in their app.

```
ArgumentError:
   wrong number of arguments (given 2, expected 1)
 # ruby/3.0.0/forwardable.rb:133:in `instance_delegate'
```

The developers reported that before the upgrade to `Disposable 0.6.0`, code as the following worked fine.

```ruby
class CreateForm < Reform::Form
  delegate :assignment, to: :model
end
```
After the upgrade, this would break with the `ArgumentError` illustrated above. My first suspicion was that this is some code not ready for Ruby 3, as this kind of exception is often observed in older code that runs with Ruby 3.

## Delegate or delegate?

An hours going through the [`Forwardable` code](https://ruby-doc.org/stdlib-3.0.0/libdoc/forwardable/rdoc/Forwardable.html) and playing with the broken example app provided by some nice user didn't bring any progress.

At some point I realized that what the users were actually using to create delegations was the `delegate` method not from `Forwardable` but from `ActiveSupport::Delegate`. The [docs enlightened me](https://apidock.com/rails/v5.2.3/Module/delegate) that this module mixes a method `#delegate` into the class - the method that was used by all users reporting the error.

## Order matters!

It feels really stupid in hindsight, and it took at least two hours to understand the problem.

Here's the chain of sparks that lead to fixing the problem.

1. A `Reform::Form` class inherits from `Twin::Disposable`. This means that each `Form` class gets `Forwardable` methods mixed in. Of course, only *after* upgrading `disposable` to 0.6.0 where we automatically included `Forwardable` into `Twin`.
3. Before 0.6.0, the former delegation from `Uber::Delegate` only mixed in one method called `#delegates` (mind the `s`). 
2. The `Forwardable` module includes a method called `#delegate`.
4. Our nasty `ActiveSupport::Delegate` module also has a method called `#delegate` - just like `Forwardable`!
5. In some Rails apps, delegation from ActiveSupport is automatically included into all classes. 

The problem we were facing was that ActiveSupport and its delegation code was included automatically into the `Reform::Form` class *before* the `disposable` gem included `Forwardable` and hence the latter overriding the `#delegate` method you are expecting.

In other words, `Forwardable` killed ActiveSupport's `#delegate` method as it was loaded and included later.

## Problem solved

We decided to [removed `Forwardable` from `disposable`](https://github.com/apotonick/disposable/commit/684a8929c495123a3e7537d2e0be1051342a007f) for now and fixed the obscure issue. When using `delegate` in a form it now refers to ActiveSupport's implementation.