LoginSignup
182
126

More than 3 years have passed since last update.

Rubyで型チェック!動かして理解するRBS入門 〜サンプルコードでわかる!Ruby 3.0の主な新機能と変更点 Part 1〜

Last updated at Posted at 2020-12-06

はじめに

Ruby 3.0ではRubyのコードに型定義情報を提供するRBSという仕組みが導入されます。
この記事では簡単なサンプルプログラムを通して、RBSとその周辺ツールの使い方や役割を説明します。
なお、説明する内容はあくまで初歩的な内容です。予めご了承ください。

動作確認時の実行環境

本記事の執筆時点ではまだRuby 3.0は正式にリリースされていません。
正式リリース時、または今後のバージョンアップによってこの記事の内容と実際の挙動が異なる可能性もあります。

本記事の執筆時に使用した実行環境は以下のとおりです。

  • Ruby 3.0.0dev (2020-11-13T16:46:08Z master 7826210541) [x86_64-darwin19]
  • rbs 0.17.0
  • typeprof 0.4.2
  • steep 0.36.0

ただし、型チェックに関わる機能はgemとして提供されているため、gemをインストールすればRuby 2.7でも動かすことができます。詳しくは後述します。

サンプルコード

本記事で使用したサンプルコードは以下のリポジトリに置いています。

本記事の位置づけ

この記事はRuby 3.0 Advent Calendar 2020 7日目の記事です。

また、Ruby 2.3のリリース時から毎年続けている、「サンプルコードでわかる!Ruby 2.xの主な新機能と変更点」シリーズのRuby 3.0バージョンでもあります。(RBS以外の新機能をまとめた記事も後日公開する予定です)

Part 2も書きました!
Ruby 3.0のその他の新機能と変更点は以下の記事にまとめました。
こちらもあわせてどうぞ。

サンプルコードでわかる!Ruby 3.0の主な新機能と変更点 Part 2 - 新機能と変更点の総まとめ

それでは以下が本編です。

自分で作ったコードに型定義を付ける

自分で簡単なRubyのクラスを作り、そのクラスに型情報を定義してRubyの型チェックがどのように実現されるのかを見ていきましょう。
以下の手順に従えば、みなさんもご自身でRuby 3.0の型チェックを体験することができます。

まず、適当なディレクトリを作って、そこに移動します。
ここでは rbs-sandbox というディレクトリを作りました。

$ mkdir rbs-sandbox
$ cd rbs-sandbox

libディレクトリを作り、その中にfizz_buzz.rbを作ります。

$ mkdir lib
$ touch lib/fizz_buzz.rb

lib/fizz_buzz.rbには以下のようなコードを書くことにします。

class FizzBuzz
  def self.run(n)
    1.upto(n).map do |n|
      if n % 15 == 0
        'FizzBuzz'
      elsif n % 3 == 0
        'Fizz'
      elsif n % 5 == 0
        'Buzz'
      else
        n.to_s
      end
    end
  end
end

さて、このFizzBuzz.runメソッドに対して、自力で型定義を書いてもいいのですが、ここではRuby 3.0に付属するtypeprofコマンドを使って型定義を自動生成してみます。

typeprofで型定義を自動生成するためには、目的のコードを実行するRubyコードが必要になります。
そこで、runner/fizz_buzz_runner.rbに次のようなコードを書いてみましょう。(注:runnerというディレクトリ名は筆者が適当に付けた名前です。RBSの予約語とかではありません)

$ mkdir runner
$ touch runner/fizz_buzz_runner.rb

runner/fizz_buzz_runner.rbには次のようなコードを書きます。

require_relative '../lib/fizz_buzz'

results = FizzBuzz.run(15)
puts results

念のため、上のコードが動作することも確認しましょう。以下のような実行結果が得られればOKです。

$ ruby runner/fizz_buzz_runner.rb
1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz

準備ができたので、typeprofを実行しましょう。
以下のコマンドを入力します。すると以下のような出力結果が得られます。
(注:本記事執筆時点ではunknown RBS type: RBS::Types::Bases::Topという謎の警告も大量に出力されます)

$ typeprof runner/fizz_buzz_runner.rb
# Classes
class FizzBuzz
  def self.run : (Integer) -> Array[String]
end

この出力結果が、typeprofによって自動的に型推論されたFizzBuzzクラスの型情報です。
(Integer) -> Array[String]とあるように、Integer型の引数を渡すと、Stringを要素に含むArrayが戻り値として返ってくることが表現されています。

次に、Steepというgemを使って、プログラムの型チェックを行ってみましょう。
SteepはRuby 3.0には付属しないため、別途インストールする必要があります。

$ gem install steep

gemをインストールしたら、steep initコマンドを実行します。

$ steep init
Writing Steepfile...

すると、次のようなSteepfileが作成されます。

# target :lib do
#   signature "sig"
#
#   check "lib"                       # Directory name
#   check "Gemfile"                   # File name
#   check "app/models/**/*.rb"        # Glob
#   # ignore "lib/templates/*.rb"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "strong_json"           # Gems
# end

# target :spec do
#   signature "sig", "sig-private"
#
#   check "spec"
#
#   # library "pathname", "set"       # Standard libraries
#   # library "rspec"
# end

SteepfileはSteepを実行する際に必要な設定ファイルです。
設定例がコメントアウトされていますが、ここではSteepfileの中身を以下のように書き換えることにします。

target :lib do
  check "lib"
  check "runner"
  signature "sig"
end

上の設定では、libディレクトリとrunnerディレクトリを型チェックの対象とし、型定義ファイルをsigディレクトリに配置するという意味になっています。(注:本記事執筆時点ではSteepfileのDSLに関する説明が見つけられなかったので、設定の意味については筆者の推測で書いています)

型定義ファイルをsigディレクトリに配置しましょう。
次のようにsigディレクトリを作り、そこにfizz_buzz.rbsファイルを作成します。

$ mkdir sig
$ touch sig/fizz_buzz.rbs

sig/fizz_buzz.rbsに先ほどtypeprofで出力された型定義情報を書き込みます。

class FizzBuzz
  def self.run : (Integer) -> Array[String]
end

これでSteepを使う準備ができました。では、steep checkとコマンドを入力してください。

$ steep check

何か起きましたか?何も起きませんね。
何も起きないということは型チェックに成功したことを意味しています。

ちゃんと不正な型を検出できるかどうかを確認するために、fizz_buzz_runner.rbを次のように書き換えてみてください。

 require_relative '../lib/fizz_buzz'

-results = FizzBuzz.run(15)
+results = FizzBuzz.run('abc')
 puts results

こうすると、型チェック時にエラーが発生するはずです(引数にIntegerではなくStringを渡したため)。

$ steep check
runner/fizz_buzz_runner.rb:3:23: ArgumentTypeMismatch: receiver=singleton(::FizzBuzz), expected=::Integer, actual=::String ('abc')

エラーが出力されたことを確認したら、fizz_buzz_runner.rbのコードは元に戻してください。

Rubyの組み込みライブラリも型チェックの対象になります。たとえば、fizz_buzz.rbで以下のようにわざと文字列に対してuptoメソッドを呼び出してみましょう。

 class FizzBuzz
   def self.run(n)
-    1.upto(n).map do |n|
+    '1'.upto(n).map do |n|
       if n % 15 == 0

これもやはり型チェックに引っかかります。(注:本記事執筆時点ではなぜか同じエラーメッセージが2回出力されます)

$ steep check
lib/fizz_buzz.rb:3:4: UnresolvedOverloading: receiver=::String, method_name=upto, method_types=(::string, ?::boolish) -> ::Enumerator[::String, ::String] | (::string, ?::boolish) { (::String) -> void } -> ::String ('1'.upto(n))

エラーが出力されたことを確認したら、fizz_buzz.rbのコードは元に戻してください。

Ruby 3.0の型チェックの基本説明は以上になります。

ここまでの説明でわかること

ここまでの説明でわかることを以下にまとめます。

  • Rubyの型情報はrbsと呼ばれるファイルに定義する(Rubyスクリプト本体に型情報は書かない)
  • rbsは自力で書いてもよいが、typeprofを使うと型推論でrbsを自動生成できる(ただし目的のコードを動かすコードが必要)
  • 型チェックの実行はRuby本体の機能でなく、外部のgemを利用する。Steepはそのうちのひとつ
  • Rubyの組み込みライブラリは最初から型情報が提供されるので型チェックの対象となる

続いてもうちょっと応用的な使い方を見ていきます。

標準ライブラリの型チェックをする

この記事ではStringやIntegerのようにrequireなしで使えるクラスを「組み込みライブラリ」(コアライブラリともいう)、DateクラスやPathnameクラスのようにrequireが必要なクラスを「標準ライブラリ」と呼ぶことにします。

この項では標準ライブラリの型チェックを利用する手順を説明します。

fizz_buzz_runner.rbを次のように修正してみましょう。
ここでは15回という固定の回数ではなく、今日の日付の日にちぶん(つまり1〜31回)FizzBuzzを実行します。

+require 'date'
 require_relative '../lib/fizz_buzz'

-results = FizzBuzz.run(15)
+results = FizzBuzz.run(Date.today.day)
 puts results

この状態でsteep checkを実行します。

$ steep check

コードに問題がないので何も起きませんね。
ではわざと.day.dyに変えてみましょう。

 require 'date'
 require_relative '../lib/fizz_buzz'

-results = FizzBuzz.run(Date.today.day)
+results = FizzBuzz.run(Date.today.dy)
 puts results

.dyは存在しないメソッドなので、steep checkで怒られるはずです。どうでしょうか?

$ steep check

あれ?またまた何も起きませんでした……。
実は標準ライブラリを型チェックの対象にするためには、Steepfileにそのライブラリ名を指定する必要があります。
というわけで、Steepfileに以下の一行を追加します。

 target :lib do
   check "lib"
   check "runner"
   signature "sig"
+  library "date"
 end

こうするとsteep checkで型エラーを検知できます。

$ steep check
runner/fizz_buzz_runner.rb:4:23: NoMethodError: type=::Date, method=dy (Date.today.dy)

gemの型チェックをする

Ruby 3.0リリース時にはRubyの組み込みライブラリや標準ライブラリについては、最初から型定義情報が同梱されます。
しかし、Ruby標準ではないサードパーティー製のgemについては、別途型定義が必要になります。

TypeScriptのDefinitelyTypedと同じように、RBSにはgem_rbsというリポジトリがあります。

本記事執筆時点では、listen、rainbow、redis、retryableという限られた種類のgemしか型情報が登録されていませんが、今後RBSが普及するにつれて、型情報が増えていくことが期待されます。(もちろんみなさん自身がコントリビュートすることも可能です!)

ここでは試しにRetryable gemの型定義ファイルを使ってみましょう。
まず、Retryable gemをインストールします。

$ gem install retryable

次にfizz_buzz_runner.rbを次のように書き換えます。(Retryableの型チェックをしたいだけなので、コード自体にあまり意味はありません)

require 'date'
require 'retryable'
require_relative '../lib/fizz_buzz'

Retryable.retryable(tries: 3) do
  results = FizzBuzz.run(Date.today.day)
  puts results
end

さらにgit submoduleを使って、gem_rbsをインポートします。

# git initしてなかったら、まずgit initを実行
$ git init

# git submoduleコマンドでgem_rbsをインポート
$ git submodule add https://github.com/ruby/gem_rbs.git vendor/rbs/gem_rbs

Steepfileを開いて、Retryableの型情報を読み込むようにします。
なお、library "forwardable"も追加しているのは、Retryableが内部的にForwardableモジュールを使っているため、この記述がないとsteep check時にUnknownTypeNameError: name=Forwardableというエラーが出るためです。

 target :lib do
   check "lib"
   check "runner"
   signature "sig"
+  repo_path "vendor/rbs/gem_rbs/gems"
+  library "retryable"
+  library "forwardable"
 end

この状態でsteep checkを実行すると、型チェックに成功=つまり何も起きないはずです。

$ steep check

ためしに.retryable.retryableeに書き換えて見ましょう。

 require 'date'
 require 'retryable'
 require_relative '../lib/fizz_buzz'

-Retryable.retryable(tries: 3) do
+Retryable.retryablee(tries: 3) do
   results = FizzBuzz.run(Date.today.day)
   puts results
 end

こうすると型チェックに引っかかるはずです。

$ steep check
runner/fizz_buzz_runner.rb:5:0: NoMethodError: type=singleton(::Retryable), method=retryablee (Retryable.retryablee(tries: 3) do)

キーワード引数も型チェックされます。3を文字列の"3"に変えてみましょう。

 require 'date'
 require 'retryable'
 require_relative '../lib/fizz_buzz'

-Retryable.retryable(tries: 3) do
+Retryable.retryable(tries: "3") do
   results = FizzBuzz.run(Date.today.day)
   puts results
 end

steep checkを実行すると、キーワード引数の型が違うと怒られます。

$ steep check
runner/fizz_buzz_runner.rb:5:20: ArgumentTypeMismatch: receiver=singleton(::Retryable), expected=::Retryable::Configuration::options, actual={ :tries => ::String } (tries: "3")

VS Code用のエクステンションで型チェックする

SteepはVS Code用のエクステンションも提供しています。

VS Codeで"Steep"という名前のエクステンションを検索すると、以下のエクステンションが見つかるはずです。
まずこれをインストールしましょう。

Screen Shot 2020-12-06 at 4.48.31.png

このエクステンションを使うためにはBundlerをセットアップする必要があります。
というわけで、bundle initを実行してGemfileを作成します。

$ bundle init

bundle initが終わったらGemfileを次のように編集し、bundle installを実行してください。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "steep"
gem "retryable"

Bundlerのセットアップが終わったら、VS Codeでrbs-sandboxディレクトリを開いてみましょう。
コードが1つ前の節で使った(tries: "3")の状態になっていれば、以下のように型エラーが通知されているはずです。

Screen Shot 2020-12-06 at 4.43.30.png

メソッド名のオートコンプリート機能も働きます(同時に型情報も表示されています)。

Screen Shot 2020-12-06 at 4.50.37.png

I1Sil38gCO.gif
カーソルをメソッドの上にhoverすると、そのメソッドの型情報も表示されます。

Screen Shot 2020-12-06 at 4.47.32.png

steep-vscodeを使った感想

静的型付け言語を使ったときのような開発体験をRubyでできるのは、なかなか新鮮でした!

ただ、筆者はRubyMineユーザーなので、個人的にはRubyMineでも早く同じような機能が提供されることを期待しています。
こちらのページによると、「コーディング支援などの機能や RBS のサポートは、Ruby 3.0 のリリースバージョンが公開された後に実装される見込み」だそうです)

typeprofと既存のrbsファイルを使って型情報の差分を取得する

すでに説明した通り、typeprofを利用すると型情報を自動生成することができます。
ただし、typeprofはコードから型を推測しているので、推測がうまくいかないときはコードの書き手が意図した通りの型情報が出力されない場合があります。

たとえば以下のようなコードがあったとします。

# lib/person.rb
class Person
  def initialize(name)
    @name = name
  end
end
# runner/person_runner.rb
require_relative '../lib/person'

Person.new('Alice')

この場合、typeprofの実行結果は以下のようになります。

$ typeprof runner/person_runner.rb
# Classes
class Person
  @name : String
  def initialize : (String) -> String
end

initializeの戻り値がStringになっていますが、コードの書き手の意図としては「インスタンス変数に名前をセットしただけ」であり、それを戻り値として返そうとしたわけではありません。

こういう場合はtypeprofの出力結果を自分で編集する必要があります。そこで、sig/person.rbsは次のように戻り値をStringからvoidに変えて保存しました。

class Person
  @name : String
  def initialize : (String) -> void
end

続けて、Personクラスにhelloメソッドを実装します。

 class Person
   def initialize(name)
     @name = name
   end

+  def hello
+    puts "Hello, I'm #{@name}!"
+  end
 end

person_runner.rbでもhelloメソッドを呼び出すようにします。

require_relative '../lib/person'

person = Person.new('Alice')
person.hello

しかし、この状態でtypeprofを再実行すると、先ほど修正したinitializeメソッドの戻り値がまたStringに戻ってしまいます。

$ typeprof runner/person_runner.rb
# Classes
class Person
  @name : String
  def initialize : (String) -> String
  def hello : -> nil
end

typeprofを実行するたびに以前修正した内容を何度も修正するのは面倒です。
この問題を避けるため、typeprofでは実行用のコードだけでなく、既存のrbsファイルも型推論のインプットとして与えることができます。

というわけで、typeprofの引数に先ほど作ったperson.rbsを指定して実行してみましょう。
こうすると、既存のrbsファイルに定義されていない型情報だけが出力されます(既存の型情報はコメントアウトされて出力されます)。

$ typeprof sig/person.rbs runner/person_runner.rb
# Classes
class Person
# @name : String
# def initialize : (String) -> void
  def hello : -> nil
end

あとは差分として出力されたdef hello : -> nilsig/person.rbsに追加すればOKです(ただし、ここも戻り値はnilではなくvoidに変えた方がいいかもしれません)。

なお、typeprof-oオプションを付けることで、自動生成された型情報をファイルに出力することもできます。

$ typeprof sig/person.rbs runner/person_runner.rb -o sig/person.gen.rbs

rbsコマンドで型定義ファイルの雛形を作成する

ここまでtypeprofコマンドを使って型定義ファイルを作成してきましたが、typeprofの代わりにrbsコマンドを使って型定義ファイルの雛形(プロトタイプ)を生成することができます。
以下はrbsコマンドでfizz_buzz.rbの型定義情報を生成する例です。

$ rbs prototype rb lib/fizz_buzz.rb
class FizzBuzz
  def self.run: (untyped n) -> untyped
end

rbsコマンドを使った場合はtypeprofのように型推論はされませんが(引数も戻り値もuntypedと出力される)、その代わりに対象のファイルを実行するプログラム(この記事でいうところのrunnerスクリプト)を用意する必要がなくなります。

rbsコマンドでメソッドシグニチャを確認する

ちなみに、rbsコマンドは雛形作成以外にもいくつか機能があります。
以下はrbsコマンドを使ってメソッドシグニチャを確認する例です。

# String#rjustのシグニチャを確認する
$ rbs method String rjust
::String#rjust
  defined_in: ::String
  implementation: ::String
  accessibility: public
  types:
      (::int integer, ?::string padstr) -> ::String

# Math.sin(クラスメソッド)のシグニチャを確認する
$ rbs method --singleton Math sin
::Math.sin
  defined_in: ::Math
  implementation: ::Math
  accessibility: public
  types:
      (::Numeric x) -> ::Float

# Date#month(requireが必要な標準ライブラリ)のシグニチャを確認する
$ rbs -r date method Date month
::Date#month
  defined_in: ::Date
  implementation: ::Date
  accessibility: public
  types:
      () -> ::Integer

# Retryable.retryable(gemのクラスメソッド)のシグニチャを確認する
$ rbs --repo vendor/rbs/gem_rbs/gems -r retryable -r forwardable method --singleton Retryable retryable
::Retryable.retryable
  defined_in: ::Retryable
  implementation: ::Retryable
  accessibility: public
  types:
      [X] (?::Retryable::Configuration::options options) ?{ (::Integer, ::Exception?) -> X } -> X?

テストコードとTypeProfを組み合わせてrbsを作成する

この記事ではfizz_buzz_runner.rbのような実行スクリプトを作りましたが、Minitestのようなテストコードを実行スクリプト代わりに使ってもかまいません。

以下はテストコードからFizzBuzzクラスのrbsを生成する例です。

test/fizz_buzz_test.rb
require 'minitest/autorun'
require_relative '../lib/fizz_buzz'

class RubyTest < Minitest::Test
  def test_run
    expected = ['1', '2', 'Fizz', '4', 'Buzz', 'Fizz', '7', '8', 'Fizz', 'Buzz', '11', 'Fizz', '13', '14', 'FizzBuzz']
    assert_equal expected, FizzBuzz.run(15)
  end
end
$ typeprof test/fizz_buzz_test.rb
# Classes
class FizzBuzz
  def self.run : (Integer) -> Array[String]
end

class FizzBuzzTest
  def test_run : -> untyped
end

FizzBuzzTestクラスのrbsも一緒に出力されていますが、FizzBuzzクラスもfizz_buzz_runner.rbを使ったときと同じようにrbsが出力されています。

その他

Railsで使うにはどうしたらいいの?(未検証)

pockeさん(@p_ck_)が開発されているrbs_railsというgemを使うと、Rails用のrbsを生成できるみたいですが、まだ未検証です。すいません。
詳しくはrbs_railsのリポジトリや、各種Web記事を参照してください。

SorbetとRBSの関係はどうなってるの?

Rubyの型チェックに以前から興味を持っていた方は、Sorbet(ソルベ)をご存じかもしれません。

SorbetはRBSやSteepより先行して開発・運用されている型チェックツールです。
基本的にSorbetとRBS/Steepは「Rubyの型チェックをする」という目的は同じであるものの、それを実現する仕組みは別物です。

とはいえ、SorbetとRubyの開発チームは互いに協力しながら、相互に互換性のある型チェックの仕組みを構築していく予定だそうです。

詳しくは下記のブログ記事(英語)をご覧ください。

実はRuby 2.7でも使えるRBS

Steepだけでなく、RBSとTypeProfもgemとして提供されています。そのため、これらのgemさえインストールすればRuby 3.0だけでなくRuby 2.7でも動作させることが可能です。

以下はRuby 2.7.2で各ツールを動かした際の実行結果です。

$ ruby -v    
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-darwin19]

$ gem install rbs
$ gem install typeprof
$ gem install steep

$ rbs prototype rb lib/fizz_buzz.rb 
class FizzBuzz
  def self.run: (untyped n) -> untyped
end

$ typeprof runner/fizz_buzz_runner.rb
# Classes
class FizzBuzz
  def self.run : (Integer) -> Array[String]
end

$ steep check

ちなみにTypeProfはRuby 2.7以上が必要ですが(参照)、RBSとSteepはRuby 2.6でも動作するようです(参考-1参考-2)。

ところでRBSって何の略?

RBSは何の略語なのか、はっきりわからないのですが、こちらのスライドから想像するに、"Ruby Signature language"の略かな?と思ったりしています。

RBS/TypeProf/Steepに関する情報源

この記事で紹介したのはHello, worldレベルのごく簡単なRBS/TypeProf/Steepの使い方です。
本格的にRubyで型チェックをしようとすると、もっともっと深い知識が必要になります。
より詳しく知りたい方は、以下の資料を参考にしてください。

RBS/TypeProf/Steepの公式リポジトリ

各リポジトリのREADMEにはさらに詳しい情報へのリンクが載っています。

設計思想に触れる

RBSとSteepの開発は松本宗太郎さん(@soutaro)が、TypeProfの開発は遠藤侑介さん(@mametter)がそれぞれ中心になって開発されています。
各ツールの設計思想はお二人の登壇スライドやWeb記事等で確認することができます。

型定義ファイルのサンプルを探す

型定義ファイルの書き方がわからない場合は、実際の型定義ファイルを見てみると参考になるかもしれません。

ブラウザ上でTypeProfを試す

下記サイトに行くと、ブラウザ上でTypeProfの実行結果を確認することができます。

Screen Shot 2020-12-06 at 9.57.31.png

まとめ(というか個人的な感想)

というわけで、この記事ではRuby 3.0から提供されるRBSとその関連ツールを使って、Rubyの型チェックの概要を説明しました。

個人的に他にもいくつかのユースケースでRBSを試してみたのですが、少し凝ったコードを書くと、型定義の書き方がよくわからなかったり、型チェック時に予期しない型エラー(従来の感覚からいうと、ちょっと厳格すぎるチェック)が発生してコードの修正方法に悩んだりしました。
なので、正直なところ、今すぐ開発の現場に投入できるかというと、まだちょっとハードルが高いんじゃないかなという印象です。
めちゃくちゃ柔軟で何でもできちゃうRubyだからこそ、外からカッチリとした型定義を与えるというのは、なかなか難易度が高い(柔軟さを取ると型定義が書きにくく、型定義をシンプルにすると柔軟さが失われやすい)、という感想を持ちました。

とはいえ、Rubyにようやく型チェックの土台が公式に整えられた、というのはとても大きな一歩だと思います。
これから徐々にgemの型定義が増えていったり、型定義を書くための知見や型を利用するノウハウが溜まっていたりしていくことで、数年後にはRubyでも「型のある世界」が当たり前になっているかもしれません。

@soutaroさんと@mametterさんの活躍に感謝しつつ、今後のRubyプログラミングの進化を楽しみにしたいと思います!

あわせて読みたい

Ruby 3.0のその他の新機能と変更点は以下の記事にまとめました。
こちらもあわせてどうぞ。

サンプルコードでわかる!Ruby 3.0の主な新機能と変更点 Part 2 - 新機能と変更点の総まとめ

182
126
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
182
126