---
# System prepended metadata

title: Desgin Doc of rbs subtract

---

# Desgin Doc of rbs subtract

This document describes rbs subtract.

## Basic Usage

You can use `rbs subtract` from command line.

```console=
# Print RBS to the stdout, which is generated.rbs - hand-written.rbs
$ rbs subtract generated.rbs hand-written.rbs

# It takes multiple minuends. The last argument becomes subtrahend.
$ rbs subtract generated-a.rbs generated-b.rbs hand-written.rbs

# It updates `.rbs` files directly with `-w` option.
$ rbs subtract -w generated-a.rbs generated-b.rbs hand-written.rbs

# It takes multiple subtrahends with --subtrahend option
$ rbs subtract generated.rbs --subtrahend=hand-written-a.rbs --subtrahend=hand-written-b.rbs
```

## The purpose of rbs subtract

`rbs subtract` focuses on conbining auto-generated RBSs and hand-written RBSs (or two kinds auto-generated RBSs).

There are several RBS generators. For example, `rbs prototype`, RBS Rails, and so on. They are useful, but the RBSs generated by them is not complete, for example, they include `untyped`.

So we want to override the generated RBSs with hand-written RBSs. But we had no good way to override them.

```rbs=
# auto-generated RBS
class C
  # This definition doesn't describe the argument and returned value types,
  # so we want to describe them with hand-written RBS.
  def m: (untyped) -> untyped
end

# hand-written RBS
class C
  # The following definition does not work with the generated RBS
  # because RBS doesn't allow duplicated method definitions.
  def m: (String) -> Integer
  
  # The following definition is valid, but it is not the expected behavior
  # because the overload still has `(untyped) -> untyped`.
  def m: (String) -> Integer | ...
end
```

To solve this problem, we need to remove `C#m` definition from the generated RBS. But modifing a generated file by the hand introduces hard maintainability sooner or later.

`rbs subtract` solves this problem. It removes duplicated definitions from the generated RBS automatically. Then we can maintain the generated RBSs.

The `rbs subtract`'s goal is modifying generated RBSs to make it valid with the other RBSs. So, after `rbs subtract a.rbs b.rbs > c.rbs`, the environment including `b.rbs` and `c.rbs` has to be valid.

## Example workflow with rbs subtract

We can use this command with the following workflow on a Rails application.

```bash=
# Generate RBSs for Active Record models under sig/rbs_rails directory
$ bin/rake rbs_rails:all

# Generate RBSs for all Ruby code under sig/prototype directory
$ rbs prototype rb --out-dir=sig/prototype --base-dir=. app lib

# Remove methods generated by RBS Rails from sig/prototype
$ rbs subtract --write sig/prototype sig/rbs_rails
# Remove hand-written methods from generated RBSs
$ rbs subtract --write sig/prototype sig/rbs_rails sig/hand-written
```


Then the sig directory contains a complete RBS files as an environment. It means `rbs -Isig validate` passes (if there is no missing classes and so on).


## Detailed specifications

See the test file.


## Implementation details

The main implementation is `RBS::Subtractor`. It subtracts an environment from declarations.

It uses `RBS::Environment` as the subtrahend.

It needs to merge several class declarations for the same class, so `RBS::Declarations` is not appropriate for this purpose.

The subtrahend RBSs is probably incomplete RBS, for example, it may depend on the minuend RBS. `RBS::DefinitionBuilder` does not work in this case, so it is inappropriate also.

## Limitations

### Interfaces mixin

`rbs subtract` is not aware of interfaces mixins. For example

```rbs=
# minuend - generated
class C
  def x: () -> untyped
end

# subtrahend - hand-written
class C
  include _I
end

interface _I
  def x: () -> untyped
end

# subtracted by `rbs subtract`
class C
  def x: () -> untyped
end
```

`x` method remains in the subtracted. Because it is actually defined by `_I`, but not `C`.

It causes duplicated method definition error, so I'd like to improve this situation. 

#### Solution ideas

* Remove entire of `class C` from subtracted if the subtrahend incldues interface mixins.
    * We can fix this problem easily.
    * But it may remove necessary methods.
* Use DefinitionBuilder to trace inheritance
    * DefinitionBuilder builds inheritance, so we can remove methods defined by interface correctly.
    * But DefinitionBuilder needs complete RBS environment.
* Search interface inheritance by the Subtractor
    * Re-implement DefinitionBuilder, but it works with incomplete environment.
    * It is bit of hard, and it doesn't 100% emulate the behavior.

### Different type parameters

The subtracted RBS doesn't work with the subtrahend if the subtrahend contains a class/module with type parameters.

```rbs=
# a.rbs
class C
  def foo: () -> untyped
end

# b.rbs
class C[T]
  def bar: () -> untyped
end

# rbs subtract a.rbs b.rbs is the following, the same as a.rbs
# The type parameter of `C` is different, so it causes an error.
class C
  def foo: () -> untyped
end
```



### attr_accessor

Currently `rbs subtract` command removes `attr_accessor` if the subtrahend contains one of the methods that `attr_accessor` defines. For example

```rbs=
# minuend.rbs
class C
  # It defines a and a=
  attr_accessor a: String
end

# subtrahend.rbs
class C
  def a: () -> String
end
```

In this case, `rbs subtract a.rbs b.rbs` prints nothing. It removes `C#a=` unexpectedly.

We can fix this problem more easily than other problems. We can convert `attr_accessor` to a `attr_{reader,writer}` in this case.

## Alternative Approaches

This section describes alternative approaches that I considered.

### Specify multiple subtrahends

#### Decided specification

`rbs subtract` treat the last argument as a subtrahend by default. But you can also specify multiple subtrahends by `--subtrahend` option. For example:

```bash=
# Specify one subtrahend
$ rbs subtract minuend.rbs subtrahend.rbs

# Specify two or more subtrahends
$ rbs subtract minuend.rbs --subtrahend=subtrahend_1.rbs --subtrahend=subtrahend_2.rbs
```


#### Why this feature is necessary

Specifying multiple subtrahends is useful on the following situaion.


```
.
└── sig
    ├── app
    │   └── models/user.rbs
    ├── lib
    │   └── lib.rbs
    ├── prototype
    │   └── app/models/user.rbs
    └── rbs_rails
        └── app/moels/user.rbs

6 directories, 4 files

```

In this case, `rbs subtract` executes `(sig/prototype + sig/rbs_rails) - (sig/app + sig/lib)`, which takes two directories as the subtrahends.


#### Alternative Solutions

I considered the following solutions too.

```console=
# Separate minuends and subtrahends by `-`
#
# It looks cool, but it is not common as CLI.
# And I'm not sure I can implement it easily with optparse, because `-` is a meta character of optparse.
$ rbs subtract sig/prototype sig/rbs_rails - sig/app sig/lib

# Add --minuend option
#
# It is not bad, but I like --subtrahend.
$ rbs subtract --minuend=sig/prototype --minuend=sig/rbs_rails sig/app sig/lib

# Add --minuend and --subtrahend options
#
# It is too redundant.
$ rbs subtract --minuend=sig/prototype --minuend=sig/rbs_rails \
  --subtrahend=sig/app --subtrahend=sig/lib

# Specify comma separated files as subtrahend
#
# I do not want to implement the comma separated files because of escaping comma.
# It will introduce complexity.
$ rbs subtract sig/prototype sig/rbs_rails sig/app,sig/lib
```

