Upgrade to Pro — share decks privately, control downloads, hide ads and more …

これからの Ruby と今の Ruby について

osyo
October 23, 2020

これからの Ruby と今の Ruby について

osyo

October 23, 2020
Tweet

More Decks by osyo

Other Decks in Programming

Transcript

  1. Ruby 3.0 でこれを実⾏すると何が起きる? Ruby 3.0 でこれを実⾏すると何が起きる? private def value(value) =

    value => @value private def value(value) = value => @value → 知りたい⼈は最後まで読んでね! → 知りたい⼈は最後まで読んでね!
  2. ⾃⼰紹介 ⾃⼰紹介 名前:osyo Twitter : github : ブログ : 趣味で

    Ruby にパッチを投げたり bugs.ruby で気になったチケットを ブログにまとめたりしてる Ruby 2.5 〜 2.7 までオレオレ機能を追加した Ruby 3.0 では機能追加できなかった… Ruby で⼀番好きな機能は Refinements オンラインなのであちこちの地域.rb に参加してる @pink_bangbi osyo-manga Secret Garden(Instrumental) Fukuoka.rb Hamada.rb kawasaki.rb nikotama.rb Sendai.rb Shibuya.rb Shinjuku.rb Tama.rb Toyama.rb Gotanda.rb Entaku.rb Grow.rb Hamamatsu.rb kanazawa.rb Kobe.rb Machida.rb mitaka.rb Ruby Tuesday toruby ⻄⽇暮⾥.rb
  3. 注意 注意 このスライドでは基本的に Ruby 3.0.0-preview1 時点での挙動を元に して書いている 特別な記述がない限りは preview1 でコードを実⾏できる(はず

    ただし、⼀部、最新版で動作確認しているので注意 Ruby 3.0 がリリースされた時に挙動が変わっている可能性がある Ruby 3.0 を使⽤する場合はその時点でのリリースノート等の情報 を参照してね
  4. Ruby 3.0 の概要 Ruby 3.0 の概要 Ruby 3.0 は 2020/12/25

    にリリース予定 オリンピックは延期しても Ruby のリリースは延期しないよ それに先駆け先⽇ がリリースされた Ruby 3.0 はいろいろと⽬⽟機能がてんこ盛りなのでみんな試してみ よう!! 実際に試して変な挙動があればどんどん に報告してね ⼀⽅で Ruby 3.0 ではいくつか⾮互換な変更が⼊っている キーワード引数の挙動などなど… バージョンを上げる際には注意する必要がある Ruby 3.0.0 preview1 bugs.ruby
  5. RBS RBS Ruby 3.0 で RBS という型情報の概念が追加される RBS は .rbs

    というファイルに型情報を定義する Ruby のコード上には型情報は記述しない .rbs の書き⽅はドキュメントをみてね RBS の利⽤⽅法は⼤きく分けて 2 種類ある 1. CLI から型情報を取得する 2. Ruby のコード上から型情報を取得する また Ruby 3.0 時点では標準の機能で RBS を利⽤した型チェックなど は⾏わない予定 RBS は gem としても配布されているので gem install rbs すれば Ruby 2.7 でも使えるよ!!
  6. CLI から RBS を利⽤する rbs コマンドから型情報を取得する # 任意のクラス/ モジュールの継承リストを取得する $

    rbs ancestors ::String ::String ::Comparable ::Object ::Kernel ::BasicObject # 任意のメソッドの型シグネチャを取得する $ rbs method ::String gsub ::String#gsub defined_in: ::String implementation: ::String accessibility: public types: (::Regexp | ::string pattern, ::string replacement) -> ::String | (::Regexp | ::string pattern, ::Hash[::String, ::String] hash) -> ::String | (::Regexp | ::string pattern) { (::String match) -> ::_ToS } -> ::String | (::Regexp | ::string pattern) -> ::Enumerator[::String, self]
  7. rbs コマンドで .rbs の雛形を⽣成する 元にする .rb ファイル # fizzbuzz.rb class

    FizzBuzz def initialize(value) @value = value end def fizz?; @value % 3 == 0 end def buzz?; @value % 5 == 0 end def fizzbuzz?; fizz? && buzz? end def to_s fizzbuzz? ? "FizzBuzz" : fizz? ? "Fizz" : buzz? ? "Buzz" : @value.to_s end end class Integer def to_fizzbuzz FizzBuzz.new(self) end end
  8. "rbs prototype rb ファイル名" で雛形を⽣成できる このファイルを元に .rbs ファイルに型情報を記述する $ rbs

    prototype rb fizzbuzz.rb class FizzBuzz def initialize: (untyped value) -> untyped def fizz?: () -> untyped def buzz?: () -> untyped def fizzbuzz?: () -> untyped def to_s: () -> untyped end class Integer def to_fizzbuzz: () -> untyped end
  9. こんな感じで実際の型情報を .rbs ファイルに保存する # fizzbuzz.rbs class FizzBuzz def initialize: (Integer)

    -> void def fizz?: () -> bool def buzz?: () -> bool def fizzbuzz?: () -> bool def to_s: () -> String end class Integer def to_fizzbuzz: () -> FizzBuzz end
  10. Ruby から .rbs ファイルを読み込んで型情報を取得できる require "rbs" loader = RBS::EnvironmentLoader.new() #

    .rbs を読み込むパスを追加 loader.add(path: Pathname("./")) environment = RBS::Environment.from_loader(loader).resolve_type_names builder = RBS::DefinitionBuilder.new(env: environment) # FizzBuzz クラスの型情報を取得 fizzbuzz = RBS::TypeName.new(name: :FizzBuzz, namespace: RBS::Namespace.root) instance = builder.build_instance(fizzbuzz) # 全インスタンスメソッドを出⼒ puts instance.methods.map { |name, sig| method.method_types.map { |sig| "FizzBuzz##{name}#{sig}" } } # output: # ... # FizzBuzz#fizz?() -> bool # FizzBuzz#buzz?() -> bool # FizzBuzz#fizzbuzz?() -> bool # ...
  11. 標準ライブラリの型情報を取得したりもできる require "rbs" loader = RBS::EnvironmentLoader.new() environment = RBS::Environment.from_loader(loader).resolve_type_names builder

    = RBS::DefinitionBuilder.new(env: environment) # String の型情報を取得する string = RBS::TypeName.new(name: :String, namespace: RBS::Namespace.root) # String#index の型シグネチャを取得する instance = builder.build_instance(string) puts instance.methods[:index].method_types.join("\n") # output: # (::Regexp | ::string substr_or_regexp, ?::int offset) -> ::Integer? # (?::string str, ?capacity: ::int, ?encoding: ::encoding) -> ::String # String.new の型シグネチャを取得する singleton = builder.build_singleton(string) puts singleton.methods[:new].method_types.join("\n") # output: # (?::string str, ?capacity: ::int, ?encoding: ::encoding) -> ::String
  12. 参照リンク 参照リンク GitHub - ruby/rbs: Type Signature for Ruby rbs/CONTRIBUTING.md

    at master · ruby/rbs · GitHub rbs cli - pockestrap Ruby の型関連の情報まとめ - Qiita
  13. TypeProf TypeProf TypeProf は Ruby のソースコードから型推論して型情報がある .rbs を ⽣成するツール 最近

    TypeProfiler から TypeProf に名前が変わった TypeProf は Ruby 3.0 に同梱される予定 開発版だとすでに これは .rb をパースする際に実際にメソッドに渡されたリテラルから 型を推論するような仕組みになっている なので .rb の書き⽅によって⽣成される .rbs は異なる また TypeProf は Ruby 3.0.0-preview1 には同梱されていないので注意 TypeProf は gem としても配布されているので gem install typeprof で 今すぐ試すことができる!! bundled gem になっている
  14. typeprof コマンドで .rbs 形式の情報を出⼒する Ruby 3.0.0-preview1 では事前に gem install typeprof

    しておく必要があ る ここに書かれているのは 2020/10/21 時点のもの # 元ファイル # twice.rb def twice(a) a + a end # typeprof コマンドで .rbs 形式の出⼒がされる # typeprof は実際にメソッドに渡される引数リテラルを元にして型推論を⾏う # 現状だと twice メソッドの呼び出しがないので型推論できない… $ typeprof twice.rb # Classes class Object def twice : (untyped) -> untyped end
  15. TypeProf はメソッド呼び出しのリテラルから型推論する # twice.rb def twice(a) a + a end

    # メソッドを呼び出す twice(42) twice("homu") $ typeprof twice.rb # 型情報が追加されている # twice に Integer や String が渡されている情報を元にして型推論する # Classes class Object def twice : (Integer | String) -> (Integer | String) end
  16. 変数やメソッドを経由しても推論される、すごい def twice(a) a + a end value = 42

    value2= twice(value) # Integer を渡す twice(value2.to_s) # String を渡す $ typeprof twice.rb # Classes class Object def twice : (Integer | String) -> (Integer | String) end
  17. 複雑なコードを渡すと… ? class FizzBuzz def initialize(value) @value = value end

    def fizz?; @value % 3 == 0 end def buzz?; @value % 5 == 0 end def fizzbuzz?; fizz? && buzz? end def to_s fizzbuzz? ? "FizzBuzz" : fizz? ? "Fizz" : buzz? ? "Buzz" : @value.to_s end end class Integer def to_fizzbuzz FizzBuzz.new(self) end end puts 1.upto(20).map { |index| index.to_fizzbuzz }
  18. こういう結果になる かなり便利そうなので期待したい $ typeprof fizzbuzz.rb # Classes class Integer <

    Numeric def to_fizzbuzz : -> FizzBuzz end class FizzBuzz @value : Integer def initialize : (Integer) -> Integer def fizz? : -> bool def buzz? : -> bool def fizzbuzz? : -> bool def to_s : -> String end
  19. 参照リンク 参照リンク ⽇本語の資料なので読みやすい! ここを読めばだいたい OK RubyKaigi Takeout 2020 の登壇動画 RubyKaigi

    Takeout 2020 の登壇資料 GitHub - ruby/typeprof: An experimental type-level Ruby interpreter for testing and understanding Ruby code typeprof/doc.ja.md at master · ruby/typeprof · GitHub [JA] Type Profiler: a Progress Report of a Ruby 3 Type Analyzer / Yusuke Endoh @mametter - YouTube Type Profiler: Ambitious Type Inference for Ruby 3
  20. Ractor Ractor Ractor は Ruby で並列処理を⾏うためのライブラリ 以前は Guild という名前で開発されていたが Ractor

    という名前に 変わった Ruby + Actor の略 Ruby 3.0 では実験的に導⼊される予定 Ractor に関しては⽇々改良されているので preview1 と⽐較して Ruby 3.0 がリリースされる頃にはいろいろと使い⽅や制限が変わっている 可能性があるので注意する 最近も #recv から #receive に名前が変わった サポートする共有可能オブジェクトが追加されたりとか
  21. 以下のコードを実⾏すると hello と world が混ざって出⼒される # Ractor.new のブロックが並⾏処理として実⾏される # ブロック内は外の変数を参照できないので

    # .new の引数がブロックの引数として受け取る事ができる ractor = Ractor.new(10) do |loop_count| # トップレベルとは別に Ractor 内でも設定する必要がある $stdout.sync = true loop_count.times do puts :hello sleep 0.3 end end # 出⼒バッファリングを無効にする $stdout.sync = true 10.times do puts :world sleep 0.3 end
  22. Ractor のメッセージの送り⽅:push 型 # Ractor へメッセージを送る # push 型 ractor

    = Ractor.new { $stdout.sync = true puts "=== Ractor start ===" # メッセージを受け取るまで待つ # 将来的に recv は receive に名前が変わるので注意 value = Ractor.recv puts value # => 42 puts "=== Ractor end ===" sleep 3 # ブロックの戻り値が take の戻り値になる value + value } sleep 3 ractor.send(42) # 待ち処理 puts ractor.take # => 84 puts "=== Finish ==="
  23. Ractor のメッセージの送り⽅:pull 型 # Ractor からメッセージを受け取る # pull 型 ractor

    = Ractor.new { $stdout.sync = true puts "=== Ractor start ===" # take が呼ばれるまで待つ puts Ractor.yield 42 puts "=== Ractor end ===" } sleep 3 # 待ち処理 # Ractor.yield の引数を返す puts ractor.take # => 42 sleep 2 puts "=== Finish ==="
  24. Ractor でスリープソートを書いてみるとこんな感じ 要素の値だけ sleep して要素の値が少ない順で処理するソート $stdout.sync = true puts "

    かいし!!!" data = (1..20).to_a.shuffle p data ractors = data.map { |i| Ractor.new(i) { |it| # バッファリングを無効化 $stdout.sync = true sleep it / 5.0 puts it } } # 待ち処理 ractors.map(&:take) puts " おわり!!"
  25. 参照リンク 参照リンク ⽇本語の資料だがちょっと古いので注意 ruby/ractor.md at master · ruby/ruby · GitHub

    ruby/ractor.ja.md at ractor · ko1/ruby · GitHub Ractor 超⼊⾨ - Qiita 並列処理⼊⾨ + Ruby での新しい並列実⾏単位Ractor - Qiita
  26. Module#include の挙動が変わった Module#include の挙動が変わった Ruby 3.0 で Module#include / prepend

    の挙動がちょっと変わります 今まではすでに include 済みのモジュールに対して include してもす でに include されているオブジェクトには反映されませんでした Ruby 3.0 ではあとから include した場合でもすでに include 済みのオ ブジェクトに反映されるようになりました 何をいってるのかわからないと思うのでサンプルコードを⾒てみよ う
  27. 以下のようすでに X.include M してる M に対して M.include M2 をする と

    X.ancestors に M2 が反映されるようになる module M; end class X include M end # この時点では M のみ反映されている p X.ancestors # => [X, M, Object, Kernel, BasicObject] module M2; end # M に対してあとから include したときの挙動が変わった M.include M2 # Ruby 2.7 : M2 は反映されない # Ruby 3.0 : M2 が反映される p X.ancestors # Ruby 2.7 => [X, M, Object, Kernel, BasicObject] # Ruby 3.0 => [X, M, M2, Object, Kernel, BasicObject]
  28. Ruby 3.0 からは Kernel.include M をすると既存のクラスに反映される module M def twice

    self + self end end p String.ancestors Kernel.include M # => [String, Comparable, Object, Kernel, BasicObject] # 既存のクラスに M が反映されるようになる p String.ancestors # Ruby 2.7 => [String, Comparable, Object, Kernel, BasicObject] # Ruby 3.0 => [String, Comparable, Object, Kernel, M, BasicObject] # M のインスタンスメソッドが呼び出せるようになる p "hoge".twice # Ruby 2.7 => error: undefined method `twice' for "hoge":String (NoMethodError) # Ruby 3.0 => "hogehoge"
  29. またこれは以下のように複数のモジュールを mixin している場合に 問題になる可能性がある module M1 def to_s; "M1 ->

    " + super end end module M2 def to_s; "M2 -> " + super end end class X include M1 include M2 def to_s; "X -> " + super end end # このあたりは今までどおり期待する挙動 p X.ancestors # => [X, M2, M1, Object, Kernel, BasicObject] p X.new.to_s # => "X -> M2 -> M1 -> #<X:0x000055977aa63e68>"
  30. 以下のようにすると .ancestors に同じモジュールが複数含まれるよ うになる これにより今までの挙動と少し変わる可能性がある 今までは問題なかったが Ruby 3.0 に上げるとモジュールが重複して しまう可能性があるので注意する

    # M1 に M2 を mixin する M1.include M2 # Ruby 2.7 だとモジュールは重複しないが Ruby 3.0 だと重複するようになる p X.ancestors # Ruby 2.7 => [X, M2, M1, Object, Kernel, BasicObject] # Ruby 3.0 => [X, M2, M1, M2, Object, Kernel, BasicObject] # 当然 super 経由で呼び出されるメソッドも異なるようになる p X.new.to_s # Ruby 2.7 => "X -> M2 -> M1 -> #<X:0x000055977aa63e68>" # Ruby 3.0 => "X -> M2 -> M1 -> M2 -> #<X:0x000055b8645d1d40>"
  31. 参照リンク 参照リンク 元のバグチケット 継承リストに同じモジュールが複数含まれるのはバグじゃない? というバグチケット 実際はバグではなくて意図した挙動なのでこのチケットは Reject されている Ruby 3.0

    で変わる Module#include の挙動 - Secret Garden(Instrumental) [Bug #9573] descendants of a module don't gain its future ancestors, but descendants of a class, do [Bug #17038] On master, ancestry edits can lead to duplicates in Module#ancestors
  32. 右代⼊ 右代⼊ 通常は左辺に変数に対して右辺の値を代⼊する result = 42 右代⼊は左辺の値を右辺の変数に代⼊する構⽂ 42 => result

    これを利⽤するとメソッドチェーンなど⻑いコードを書いている際 に左から右にコードを書く流れで変数を定義する事ができる irb などで変数定義する場合にいちいち左にカーソルを移動させ たりしていたがそういう煩わしさがなくなる ただし、いくつか注意点がある Ruby 3.0 では実験的な機能という位置づけ
  33. メソッドの戻り値を代⼊する場合に注意する必要がある => の優先順位の問題で以下のコードはうまく動作しない この書き⽅は twice({ 42 => result }) と解釈される

    右代⼊は処理の優先順位を考えて使う必要がある def twice(a) a + a end # twice(42) の結果を変数 reuslt に代⼊したい twice 42 => result # これは => が Hash リテラルとして解釈される為 # twice 42 => result という書き⽅は Ruby 2.7 でも有効な書き⽅になる twice({ 42 => result }) # 明⽰的に括弧を付けて代⼊する twice(42) => result p result # => 42
  34. エンドレスメソッド定義 エンドレスメソッド定義 1 ⾏でメソッドが定義できる構⽂ def value() = 42 みたいに定義できる エンドレスとは『end

    がない』という意味 Ruby 3.0 では定義できるメソッドの仕⽅に制限があるので注意する 元々はエイプリルフールネタで投稿されたチケットがなぜか実装さ れてしまい、その流れで⼊った Ruby 3.0 では実験的な機能という位置づけ
  35. def メソッド名() = 式という形でメソッドを定義することができる class FizzBuzz def initialize(value) = @value

    = value def fizz?() = @value % 3 == 0 def buzz?() = @value % 5 == 0 def fizzbuzz?() = fizz? && buzz? def to_s() = fizzbuzz? ? "FizzBuzz" : fizz? ? "Fizz" : buzz? ? "Buzz" : @value.to_s end class Integer def to_fizzbuzz() = FizzBuzz.new(self) end puts 1.upto(20).map { |index| index.to_fizzbuzz }
  36. 通常のメソッド定義とは違いいくつか制限がある 必ず () を書く必要があり = が付いているメソッドは定義できない # OK def set_value(value)

    = @value = value def value() = @value def invalid?() = @value.nil? # NG # () を付ける必要がある # syntax error, unexpected '=', expecting ';' or '\n' def value = 42 # 名前に = がついてるメソッドは定義できない # error: setter method cannot be defined in an endless method definition def value=(value) = @value = value
  37. まず は となる これは右代⼊よりもメソッド定義のほうが優先順 位が⾼いから def value(value) = value =>

    @value # def 式の戻り値が @value に代⼊される (def value(value) = value) => @value @value # => :value
  38. つまり は と同じ意味になる 右代⼊が => を使っているのがだいたい悪い private def value(value) =

    value => @value # (def value(value) = value) がキーで値が @value の Hash を private に渡そ うとする # error: `private': {:value=>nil} is not a symbol nor a string (TypeError) private( { (def value(value) = value) => @value } )
  39. その他:細かい変更点や追加点 その他:細かい変更点や追加点 キーワード引数周りで⾮互換な変更が⼊る 今まで出てた警告がエラーになる Hash#except, ENV.except が追加された ほしかったやつ Symbol#name が追加された

    ⾃⾝を frozen string で返すメソッド _1 という名前の変数やメソッドは定義できなくなる (...) 引数で第⼀引数を仮引数で定義できるようになった 詳しくは をみてくれよな! def method_missing(meth, ...) send(:"do_#{meth}", ...) end Ruby 3.0.0-preview1 の NEWS
  40. Ruby 2.7.2 の世界 Ruby 2.7.2 の世界 先⽇ された このリリースでは webrick

    の脆弱性対応が含まれている また『⾮推奨な警告がデフォルトでは出⼒さなくなる』という対応 も含まれている 今回話すのはこの『⾮推奨な警告がデフォルトでは出⼒さなくな る』ことに対して Ruby 2.7.2 がリリース
  41. 明⽰的に⾮推奨な警告を出⼒する⽅法 明⽰的に⾮推奨な警告を出⼒する⽅法 明⽰的に⾮推奨な警告を出⼒する⼿段として 2 つある 1 つは Ruby のコマンドラインオプションに -W:deprecated

    を渡す ruby -W:deprecated sample.rb もう1 つは Ruby のコード上で Warning[:deprecated] = true を呼び出す この2 つは Ruby 2.7 から使える機能になる なので Ruby 2.6 とかで使おうとするとエラーになる Ruby 2.6 の環境と混同する場合はワークアラウンド対応が必要…
  42. どうやって制御する? どうやって制御する? コマンドオププションで制御する?Warning[:deprecate] で制御す る? 環境変数 RUBYOPT に -W:deprecated を追加すればいい?

    でも Ruby 2.6 でこのオプションを参照すると死ぬ… Ruby 2.7 のプロジェクトでのみ RUBYOPT に追加したい Warning[:deprecate] = true する場合どのタイミンで呼び出す? Rails のプロジェクトだと ./bin/rails に直接記述してしまうとか? gem を開発する場合 Warning[:deprecate] を書き換えて運⽤するのは 難しそう…
  43. 個⼈的な結論 個⼈的な結論 いろいろとあって Ruby 2.7.2 ではデフォルトで出なくするという選 択肢を選んだ しかし、今まではデフォルトで警告が出ること前提で運⽤してきた なのでいずれにしろ警告⾃体を出すようにする事⾃体は問題ないは ず

    その上でどういう⼿段を⽤いて警告を出すようにするのか考えてい く必要がある 他にもいい運⽤の仕⽅を思いついたらどんどんフィードバックして ほしい 個⼈的にはアプリケーションコードの警告は出⼒して、gem の警告 は出⼒しないようにするなど細かい制御をしていきたい warning-gem とか試してみたい いい運⽤⽅法があったら教えて!! いい運⽤⽅法があったら教えて!!
  44. まとめ まとめ Ruby 3.0.0 preview1 が出たからみんな試してみてね バグとかあったらフィードバックがほしい rbs や typeprof

    は gem install でシュッと試せるよ! Ruby 2.7.2 から⾮推奨な警告がデフォルトで出なくなるよ どう運⽤していくのか考えるとむずかしい 警告⾃体は問答無⽤で出してしまってしまうのがよいと思う どう運⽤していくのがいいのかフィードバックがほしい Ruby 3.0 はまだ開発中なので実際にリリースされる時に挙動が変わ っている可能性があるので注意しましょう オンライン勉強会は場所を選ばず参加できるので参加し得 今の時期しかできないよ!!