Ruby3.0で導入される型定義！TypeprofでBlockやProcを解析してみる

この記事は[Ruby 3.0 Advent Calendar 2020](https://qiita.com/advent-calendar/2020/ruby3)の14日目の記事です。Rubyに導入される型定義でProcがどのように書かれるのかについてまとめています。

### はじめに
Ruby3.0ではコードに型定義を与える[RBS](https://github.com/ruby/rbs)という仕組みが加わります。Rubyで動いているプロダクトの可読性や保守性を高めるためにこの型定義の導入を考えている方も多いのではないでしょうか。
RBSによってRubyのコードに型情報を与えるための手助けをしてくれるツールとして静的解釈に基づく型解析ツールである[Typeprof](https://github.com/ruby/typeprof)があります。RBSやTypeprofの基本的な解説は色々な記事が出ているため、本記事ではドキュメントなどにあまり載っていないTypeprofを用いたBlockやProcの型推論に関してまとめました。

#### 使用した環境は以下の通りです
ruby 3.0.0preview1 (2020-09-25 master 0096d2b895) [x86_64-darwin18]
typeprof (0.9.0)
steep (0.37.0)
rbs (0.20.1)

#### 準備
Procを用いた簡単なコードを準備します。

```ruby

# lib/app.rb

class App
  def foo(n)
    n.to_s
  end

  def boo(fn)
    fn.call
  end

  def baz(fn)
    fn.call(5)
  end

  def bar(&fn)
    fn.call(0)
  end
end

proc1 = proc { "ブロック" }
proc2 = Proc.new { |n| n.to_s }

app = App.new

p app.foo(5)
p app.boo(proc1)
p app.baz(proc2)
p app.bar { |a| a.to_s }
```

まず実行してみます。

```ruby

$ ruby lib/app.rb
"5"
"ブロック"
"5"
"0"
```

問題なく実行できました。それではtypeprofで型解析を行います。

```

$ typeprof lib/app.rb
# Revealed types
#  lib/app.rb:24 #=> String
#  lib/app.rb:25 #=> String
#  lib/app.rb:26 #=> untyped
#  lib/app.rb:27 #=> String

# Classes
class App
  def foo: (Integer) -> String
  def boo: (^-> String) -> String
  def baz: (Proc) -> untyped
  def bar: { (Integer) -> String } -> String
end
```
型解析結果が出てきました。おおよその型をコードを実行することで判断してくれます、自分で一からRBSを書く手間が省けてありがたいですね。[Typeprofのドキュメント](https://github.com/ruby/typeprof/blob/master/doc/doc.ja.md)によるとProcに関しては抽象化せず基本的に渡される引数と返される値を元に具体的な型を出力するようです。
>Procオブジェクトは、ラムダ式（-> { ... }）やブロック仮引数（&blk）で作られるクロージャです。 これらは抽象化されず、コード片と結びついた具体的な値として扱われます。 これらに渡された引数や返された値によってRBS出力されます。

Typeprocではコードを実行することで型情報の解析をするため、どのような分析結果が出るかはコードの使用例によって異なります。今回のコードではbooには引数なしでStringを返すprocを与えているため(^-> String) -> Stringと分析されていますが、例えば以下のようにこれをintを返すprocに変更すると、typeprofの分析結果は変わります。

```ruby
proc1 = proc { 1 }
p app.boo(proc1)

#=> def boo: (^-> Integer) -> Integer
```
また、これら両方を与えると、出入力両方がユニオンタイプの型シグネチャになります。

```ruby
proc1 = proc { "ブロック" }
proc2 = proc { 1 }
p app.boo(proc1)
p app.boo(proc2)

#=> def boo: (^-> (Integer | String)) -> (Integer | String)
```



Procやblockの型シグネチャは以下のように行います。詳細は[RBSのsyntaxドキュメント](https://github.com/ruby/rbs/blob/master/docs/syntax.md)を参照してください。

```ruby

# Proc
^(Integer) -> String
^(?String, size: Integer) -> bool
#Block
{ (Integer) -> (Integer | String) }
```

booに関してはうまく入出力の型の分析ができていますが、bazに関しては入力がProcであることしか分析できていませんね。ドキュメントを調べていくと以下のような文を見つけました。
>Class.newは対応されません（untypedを返します）。 

これが直接の原因かは検証できていませんが、上記のbazに与えるproc2もproc関数で書いてみます。

```ruby
proc2 = proc { |n| n.to_s }
p app.baz(proc2)

# => def baz: (^(Integer) -> String) -> String
```
今度はちゃんと型が分析されましたね。
それでは、この結果をそのままRBSファイルにしていきます。RBSファイルの拡張子は.rbsです。

```ruby

# sig/app.rbs
class App
  def foo: (Integer) -> String
  def boo: (^-> String) -> String
  def baz: (Proc) -> untyped
  def bar: { (Integer) -> String } -> String
end
```

ではこの型情報を元に実行ファイルの型情報のチェックを行っていきます。型シグネチャを用いた静的チェックには[steep](https://github.com/soutaro/steep)というgemを使います。以下のコマンドでsteepのための設定ファイルを作成します。
 
```
$ steep init
```
生成されたSteepfileを以下のように変更します。signatureでRBSの型シグネチャファイルのあるディレクトリを、checkでチェックしたいrbファイルのディレクトリを指定します。

```ruby

target :lib do
  signature "sig"
  check "lib"
end
```
型情報の静的チェックを行います。

```
$ steep check
lib/app.rb:25:10: ArgumentTypeMismatch: receiver=::App, expected=^() -> ::String, actual=::Proc (proc1)
lib/app.rb:26:10: ArgumentTypeMismatch: receiver=::App, expected=^(::Integer) -> ::String, actual=::Proc (proc2)
```
型チェックのエラーが出ましたね。どうやらsteepではまだProcの引数のチェックまではできず、Procとプロック型シグネチャを不一致と見なしてエラーを吐いてしまうようなので、以下のように書き換えます。

```ruby

# sig/app.rbs
class App
  def foo: (Integer) -> String
  def boo: (Proc) -> String #<=ここを編集
  def baz: (Proc) -> String #<=ここを編集
  def bar: { (Integer) -> String } -> String
end
```
もう一度型チェックを行います。

```
$ steep check
```
今度はエラーが出ませんでした。steepがprocの引数の型までチェックしてくれるようになるといいですね。
最終的なフォルダ構成は以下のようになっています。また、使用したコードは[https://github.com/TomeHirata/typeprof_test](https://github.com/TomeHirata/typeprof_test)に上がっています。
![スクリーンショット 2020-12-08 21.43.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/316582/237e3121-56e2-5b17-9820-2a5a9a3d4db6.png)

### まとめ
Ruby3.0で標準となるRBSやTypeprofを用いてrubyコードの型プロファイリングを行なってみました。自動で既存のRubyコードからRBSの型定義を分析してくれるTypeprofとても便利そうですね！Typeprofでおおよその型定義を自動生成して一部手直しすることで簡単に型定義が用意できるようになりそうです。
RBSではProcとBlockはそれぞれ以下のように書きますが、本記事執筆時点ではTypeprofでClass.newをおってくれなかったり、steepでprocの型不一致エラーが起きたりしました。

```ruby

# Proc
^(Integer) -> String
#Block
{ (Integer) -> (Integer | String) }
```
まだ、型シグネチャの書き方や解析ツールの推論部分などにわかりにくい部分はありますが、今後改善されていくと思います。皆さんもぜひこの機会に型を持ったRubyライフを始めてみてください。また、このように書くとsteepやTypeprofでProcの定義がうまくいくよと言ったTipsがあればぜひ教えてください！！

### 最後に
現在 estie では, JavaScript や Ruby に強いエンジニアを積極採用中です！！

不動産のデータを使ってデータプラットフォームを構築したい、分析したい、プロダクトを作ってみたいという方はぜひ！

[他のブログ](https://inside.estie.co.jp/)や[採用ページ](https://www.estie.co.jp/#recruit)をご覧ください。