RBS から始める

静的型付け生活

Leaner Technologies Inc.

黒曜
(@kokuyouwind)

$ whoami

  • 黒曜 / @kokuyouwind

  • 名古屋在住

  • Leaner Technologies Inc. 所属

  • Railsエンジニア

  • Next.js とか AWS 周りも触ってる

  • We're Hiring!!!

静的型付けの話

2020年12月の Ruby 3.0
静的型検査の仕組みが導入された

2021年12月の Ruby 3.1 でも
各種のアップデートが入っている

Rubyの静的型検査の概要と、
どう活用するかの話をします

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

動的型付けと静的型付け

  • 動的型付け

    • 事前の型検査は行わず、実行時に型を検査する

    • 型エラーは実行時にしかわからない

    • 動的なメソッド追加などに対応しやすい

  • 静的型付け

    • コンパイル時など、実行前に型を検査する

    • 型エラーを早期に検知できる

    • 動的型付け言語への静的型検査機能追加が増えている

Rubyは動的型付け言語

Rubyの静的型検査機能: スタンス

Rubyは生まれた時から動的型付け言語で,
型指定が無くてもこれまで動いているため,
型指定はむしろ積極的に外すべきだと
まつもとさんは主張します。

ー Ruby Kaigi 2016 基調講演レポートより

Rubyの静的型検査機能: 構成

  • RBS: Rubyのための型記述言語

  • Steep: 静的型検査器

  • TypeProf: 静的型解析器

class Stack
  def push(e)
    @stack << e
  end
end

stack.rb

class Stack
  def push: (Integer) -> untyped
end

stack.rbs

TypeProfで
型推論

Steepで
型検査

Rubyの静的型検査機能: RBS

  • RBS: Rubyのための型記述言語

    • .rb ごとに、対応する .rbs ファイルに記述

    • TypeScriptの.d.tsのようなイメージ

class Stack
  def push(e)
    @stack << e
  end
end

stack.rb

class Stack
  def push: (Integer) -> untyped
end

stack.rbs

TypeProfで
型推論

Steepで
型検査

Rubyの静的型検査機能: RBS

  • Steep: 静的型検査器

    • RBS を元に、Rubyコードの型エラーを検査

    • tsc --noEmit のようなイメージ

class Stack
  def push(e)
    @stack << e
  end
end

stack.rb

class Stack
  def push: (Integer) -> untyped
end

stack.rbs

TypeProfで
型推論

Steepで
型検査

Rubyの静的型検査機能: RBS

  • TypeProf: 静的型解析器

    • Rubyコードを元に、型を推論してRBSを生成

    • あまり他の言語では見ない仕組み

class Stack
  def push(e)
    @stack << e
  end
end

stack.rb

class Stack
  def push: (Integer) -> untyped
end

stack.rbs

TypeProfで
型推論

Steepで
型検査

(余談) 静的型検査ツール Sorbet

  • Stripe製の静的型検査ツール

    • RBSではなくRBIという独自形式を用いる

    • インラインで型注釈を書くことも可能

  • RBSを用いるツール群とは現状で互換性がない

# typed: true
extend T::Sig
sig {params(name: String).returns(Integer)}
def main(name)
  puts "Hello, #{name}!"
  name.length
end

ここまでのまとめ

  • Ruby は型宣言のための文法は導入しない方針

    • 代わりに型宣言専用のRBSが導入された

  • RBSを用いる代表的なツールが Steep, TypeProf

    • Steep を用いて静的型検査が行える

    • TypeProf を用いて型推論と簡易的な検査が行える

  • Sorbet はRBIという独自記法を用いる

    • RBSとは現状で互換性がない

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

静的型検査を利用した開発手順

  大まかに2つの手法が存在する

  • RBS をすべて記述する

    • Steep による厳密な型検査が可能

    • 他の利用者にも型情報を提供できる

    • (TypeProfでRBSの雛形を作ることも可能)

  • RBS を記述せず、Ruby コードから推論させる

    • TypeProf による簡易的な型検査が可能

    • 作業量を増やさず型の恩恵が得られる

実際に試して違いを見てみよう!

例題: IntStack

stack = IntStack.new

stack.push(1)
stack.push(2)
stack.push(10)

p stack.pop # => 10
p stack.pop + stack.pop # => 3

デモ
(Steepを利用した開発手法)

Steepfile

target :app do
  # 検査対象ディレクトリ
  check "lib"
  # RBS配置ディレクトリ
  signature "sig"

  # 検査レベルの指定
  configure_code_diagnostics(
    Steep::Diagnostic::Ruby.all_error
  )
end

RBSにpushの定義を記述すると、Ruby側でエラー表示

マウスホバーで型エラー内容を表示

push, pop の型を記述し、型だけ合わせて実装

(まだスタックとしては動かない)

インスタンス変数に配列を持つように変更
(push, popそれぞれで型エラー)

pushはnilを返すよう変更

popは @stack が空のときにデフォルト値を返すよう変更

デモ
(TypeProfを利用した開発手法)

型を推論させるため、利用側コードから記述

(この時点では undefined method)

型をあわせてpush, popを定義

Stackの機能を実装

デモ
(TypeProfでRBSの雛形を作る)

Steepを利用した実装から、RBSをすべて消した状態

(型エラーが出ている)

TypeProf で 型解析して RBS に出力

ここまでのまとめ

  • RBSを書くとコストは掛かるが各種メリットがある

    • Steepで静的型検査を厳密に行える

    • 型定義ファイルをライブラリ利用者にも提供できる

  • TypeProfのみでもある程度の型エラーが検知できる

    • 型記述のコストなしで恩恵が得られる

  • IDE拡張でエラー表示などの開発サポートが得られる

    • Language Server Protocolで実現されているため、
      VSCode以外でも利用可能

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

RBSの機能と利用例

  • RBSには高度な型を表現するための各種機能がある

    • Union Type ( T | U )

    • Generics ( T[U] )

    • Covariant, Contravariant( out T, in T )

    • etc...

  • 機能を知るための資料

ジェネリクスを使った型定義

例題: Stack

stack1 = Stack.new
stack.push(1)
stack.push(2)
p stack.pop + stack.pop # => 3

stack2 = Stack.new
stack.push('a')
stack.push('b')
p stack.pop + stack.pop # => 'ba'

stack1.push('a') # TypeError

デモ
(Stackの実装)

IntStack を Stack にリネーム

Stack#push に文字列を渡すと型エラー

Stack の型定義を Stack[T] にし、Integer を T に置き換え

(pop でデフォルト0を返すところが型エラー)

initialize で default を受け取るように変更

Integer でも String でも動くようになり、

Integer をデフォルトにしたstackに String を push すると型エラー

参考: Bounded Generics

class PrettyPrint[T < _Output]
  interface _Output
    def <<: (String) -> void
  end

  attr_reader output: T
end

  昨年末リリースされた Ruby 3.1 (RBS 2.0) で追加
(Steepでの型検査はまだ動かない模様)

ここまでのまとめ

  • RBS には高度な型を表現するための各種機能がある

    • rbsのsyntaxドキュメントを見ると一通り把握できる

  • 一例としてジェネリクスを紹介した

    • 「任意の型に関する型」を表現できる

    • Ruby 3.0 ではBounded Genericsが入り強化された

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

ライブラリ利用時の静的型検査

  • 外部ライブラリの型情報をどうするか

    • gem_rbs_collection に型情報レジストリがある

    • 型が提供されていないものは、自分で書くか
      その部分の型検査を緩める必要がある

  • rbs collection コマンドで Gemfile から依存を解析し、
    対応する型情報を gem_rbs_collection から取得できる

    • Steep などのツールの依存性解決もやってくれる

デモ
(RBS Collectionを利用した開発手法)

外部Gemの ULID と KSUID を使うコード

(いずれも型情報がないため untyped でエラー)

rbs_collection.yaml に設定を記述し、

rbs collection install を実行

コミュニティで型情報が提供されている ULID は

型が解決できるようになった

KSUID は自分で RBS を記述することで型を解決

( vendor/rbs 以下も sig ソースとして Steepfile に設定)

Steepの検査設定をstrictなどにして

untyped の型検査をしないという選択肢もある

Rails利用時の静的型検査

  • Rails では動的なメソッド定義が多いため型定義が大変

  • 弊社でもまだ導入できていないので今後の課題

    • 型検査のレベルを緩めて、重要なロジックから
      型情報を充実させていく戦略が良さそう

  • (デモは時間の都合上割愛)

アジェンダ

  • Ruby の静的型検査機能の概要

  • 静的型検査を利用した開発手順

  • RBSの機能と利用例

  • ライブラリ利用時の静的型検査

  • まとめ

まとめ

  • 静的型検査を活用したRubyの開発について紹介した

    • RBS を書いて Steep で型検査すると、
      静的型付け言語のような開発体験・安全性が得られる

    • TypeProf を使うと、これまでと同じ感覚で
      Rubyを書きながら型の恩恵が(多少)得られる

    • IDE拡張を利用すると良好な開発体験を得られる

  • RBS自体や周辺ツールもどんどん進化している

  • ruby-jp Slackの #types チャンネルに入ろう!