yensaki’s blog

必要なものは締め切り

RubyKaigi 2018 型の話のまとめ

RubyKaigi 2018 では型についてのセッションがいくつかあり、個人的になるほどと思いながら色々考える機会になりました。
ざっくりここでまとめます。

matz KeyNote 質疑応答

質問者: 最近、TypeScript など静的型付けが流行ってるのが個人的に腹が立っている。 でもJavaScriptよりはTypeScriptがいいと思っている。 Ruby に今後、静的型付けという概念が入りますか?

Matz: まずRubyは今後も静的型付けを入れる事はないです。 型があった方が便利なケースは確かにある。例えばコンパイル時に判定できるなど。 でも十分に賢い人間であれば大きくないソフトウェアであれば型を推測することができると思います。 人間にできるということは未来では優秀なコンピュータなら型宣言がなくても型を推測することができるようになるのでは? 2010年台、2020年台、今は型宣言がとても効果的なのかもしれない。 でも2030年、2040年には自動型推論が主流になって、型宣言が古めかしいものになるかもしれない。


トレンドに逆らってまで頑なに型宣言を入れるつもりがないモチベーションを知ることができた気がします。

どこかで matz さんが言ってたと思うのですが 「機械に合わせて面倒なことをしたくない。機械に面倒なことをさせたい。」みたいな話かなと。

※免責: 発言そのまま書き起こしではありません。 ニュアンスなど多々違います

Steep

https://github.com/soutaro/steep

https://speakerdeck.com/soutaro/ruby-programming-with-type-checking

  • sig/*.rbi に定義を置く
  • 実装と定義ファイルを比較してチェック
  • 実装から定義ファイルの scaffold を作成できる
  • Rails のようなメタプログラミングには効きにくい

 

Usage

以下 https://github.com/soutaro/steep の README より抜粋

 

class Person
  @name: String
  @contacts: Array<Email | Phone>

  def initialize: (name: String) -> any
  def name: -> String
  def contacts: -> Array<Email | Phone>
  def guess_country: -> (String | nil)
end

class Email
  @address: String

  def initialize: (address: String) -> any
  def address: -> String
end

class Phone
  @country: String
  @number: String

  def initialize: (country: String, number: String) -> any
  def country: -> String
  def number: -> String

  def self.countries: -> Hash<String, String>
end
class Person
  # `@dynamic` annotation is to tell steep that
  # the `name` and `contacts` methods are defined without def syntax.
  # (Steep can skip checking if the methods are implemented.)

  # @dynamic name, contacts
  attr_reader :name
  attr_reader :contacts

  def initialize(name:)
    @name = name
    @contacts = []
  end

  def guess_country()
    contacts.map do |contact|
      # With case expression, simple type-case is implemented.
      # `contact` has type of `Phone | Email` but in the `when` clause, contact has type of `Phone`.
      case contact
      when Phone
        contact.country
      end
    end.compact.first
  end
end

class Email
  # @dynamic address
  attr_reader :address

  def initialize(address:)
    @address = address
  end

  def ==(other)
    # `other` has type of `any`, which means type checking is skipped.
    # No type errors can be detected in this method.
    other.is_a?(self.class) && other.address == address
  end

  def hash
    self.class.hash ^ address.hash
  end
end

class Phone
  # @dynamic country, number

  def initialize(country:, number:)
    @country = country
    @number = number
  end

  def ==(other)
    # You cannot use `case` for type case because `other` has type of `any`, not a union type.
    # You have to explicitly declare the type of `other` in `if` expression.

    if other.is_a?(Phone)
      # @type var other: Phone
      other.country == country && other.number == number
    end
  end

  def hash<i></i>
    self.class.hash ^ country.hash ^ number.hash
  end
end
  • attr_reader など解釈されない定義は @dynamic で指定して解決する

Type Check

実装が型定義から外れていないかチェックできる

$ steep check lib
lib/phone.rb:46:0: MethodDefinitionMissing: module=::Phone, method=self.countries (class Phone)

Scaffolding

実装から型定義の枠組みを作成できる

$ steep scaffold lib/*.rb

Type Profiler

Endoh さんセッション

  • matz 曰く static type check で annotation は別ファイルが希望
  • Type DB
    • 複数の Type Profiler を集めて推定する
  • Type Profiler
    • 厳密な type interface は無理
    • 大分して静的な解析, 実行時解析

感想

前年も型についてはセッションがありましたが、より具体的な方向性が見えてきたように思います。 Ruby の楽しさを失わずに型と仲良くできる道、とても楽しそう。