そろそろ RBS と Steep について勉強しておくか,と思ったら第一歩からつまづいた話。
試す
単純なスクリプトを用意
こういうスクリプトがあったとする:
module M
def double(x)
x * 2
end
end
x は数値でも文字列でも配列でも動作するが,ひとまず Integer 前提で実装したとしよう。
RBS を書く
先ほどのスクリプトの RBS を書く。
よく知らないが,RBS ファイルは,パスの lib を sig に変え,拡張子の .rb を .rbs に変えたところに書く慣習があるらしいので,それに倣ってみる。
入門記事などによると,以下のように書けばよいはずだ。
module M
def double: (::Integer) -> ::Integer
end
(現時点で Qiita は RBS のシンタックスハイライトに対応していないようだ,悲しい)
モジュール M を使うコードはまだ書いてないが,この時点でも Steep による検査は可能だ。
Steep で検査
Steep で検査してみる:
steep check
本来はまず steep init して Steepfile というファイルを生成し,それを適切に編集してから行う。
実際,Steepfile 無しでやったので
ERROR: [Steep 1.10.0] Cannot find a configuration at Steepfile:
steep initto scaffold.
というエラーは出る。
しかし,それでも一応検査はできるようだ。
検査結果として
No type error detected.
が出た。
ここまでは順調であった。
型を少し変えてみる
double メソッドの仕様として,Integer だけでなく他の数値型も OK としたい。
モジュール M の定義はそのままでいい。
RBS を変更
RBS の Integer を Numeric に変えればよいはずだ。
module M
def double: (::Numeric) -> ::Numeric
end
再び Steep で検査
これで
steep check
してみる。
おやっ?
lib/m.rb:3:6: [error] Type `::Numeric` does not have method `*`
│ Diagnostic ID: Ruby::NoMethod
│
└ x * 2
~
Detected 1 problem from 1 file
ど,どういうことだ?
Numeric が * メソッドを持っていないだと?
んなワケあるかいっ!
Numeric に * は無いのか
いや,Numeric に * が定義されていないのは事実だ。
Numeric は抽象クラスであって,ここに直接定義されていない数値演算メソッドはいくつもある1。
* はサブクラスでそれぞれ定義されていて,Numeric には定義されていない。
だから,Steep が言っていることはいちおう筋が通っている。
しかし納得しがたいものはある。
これを Steep の限界と見るべきか Numeric の不備2と見るべきかはよく分からないが。
どう書けばよいのか
複数列挙
RBS では,型を | で繋いで併記することができる。
Integer でも Float でもよい,となると以下のように書く。
module M
def double: (::Integer | ::Float) -> ::Numeric
end
これならば検査にパスする。
しかし,数値クラスとしては Rational もあるし,場合によっては Complex にも対応させたいかもしれない。
BigDecimal はどうするんだ?
それら全ての可能性があるなら全部書くわけ?
Numeric の RBS を書く
他の方法を探る。
Numeric には * は定義されていないが,必ず(サブクラスが)* を持つ。
それならば Numeric の RBS を書いて * を定義してやればいいのでは?
以下のファイルを用意する:
class Numeric
def *: (::Numeric) -> ::Numeric
end
これで steep check すれば,モジュール M の型定義が
module M
def double: (::Numeric) -> ::Numeric
end
のままでも検査にパスするようになった。
めでたしめでたし。
……なのか?
::Numeric does not have method... と言われるたびにこういう型定義を記述しなければならないのか?
real を指定(追記 1)
(本節は投稿後に追記)
投稿後に real という型を知った。これは
と定義されており,要するに Integer か Float か Rational か,ということらしい。
言い換えれば,Numeric の組込みサブクラスで Complex を除いた全て,ということになる。
確かに「実数のすべての数値型」にしたいことは非常に多い。
今回のケースもそうだと仮定すると
module M
def double: (real) -> real
end
と書けばよいわけだ。
ただ,Complex や BigDecimal にも対応させたい場合は使えない。
型を定義(追記 2)
前節の結果を見て,「もしや型って自分で定義できるのでは?」と思った。
試しに
module M
type numeric = Integer | Float | Rational | Complex
def double: (numeric) -> numeric
end
とやってみたら,これで検査にパスした。
こういうこともできるのかあ。ただ乱用はマズいかもね。
感想
型定義を整備していけば開発が捗るらしい,というのは何となく分かるのだが,型定義を書くのがめんどくさすぎると思った。