2023-02-19
#ruby
小ネタ
個人的にぱっと書く用に、静的型解析のできるスクリプト言語を探していたのですが、意外と選択肢がない。
Typeprofがいつの間にかLanguage Serverにも対応しており、
個人的に思い入れのあるRubyで静的解析出来たら理想だったので、どのくらいできるか調査しました。
上の記事を参考にさせていただきました。
現状、Rubyで型解析を行う場合、TypeProf
とSteep
という選択肢があるようですが、標準ライブラリに統合されていそうなTypeProf
を選択
記事の通りVSCodeでセットアップしました。
手順はリンク先に任せて、はまったポイントだけメモします。
rbenv
を使用してRuby環境を構築していましたが、どうも拡張機能がrbenv
を検知できず、bundler
がないと怒っていました。
単純に設定を変更するだけで対応できました。
which bundle
#> /home/xxx/.rbenv/shims/bundle
でbundlerの位置を調べて、vscodeのsettings.json
に以下を追加するだけ
{
"typeprof.server.path": "/home/xxx/.rbenv/shims/bundle exec typeprof"
}
VSCodeの出力タブを確認すると、TypeprofのLanguage Serverが途中で落ちてました。
/home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:824:in `check_send_branch': Unknown insn: #<struct TypeProf::ISeq::Insn insn=:objtostring, operands=[], lineno=42, code_range=(42,16)-(42,19), definitions=nil> (RuntimeError)
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:573:in `block in unify_instructions'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:567:in `times'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:567:in `unify_instructions'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:57:in `block in compile_core'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:56:in `each'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:56:in `compile_core'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/iseq.rb:19:in `compile'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/builtin.rb:627:in `file_load'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/builtin.rb:726:in `kernel_require_relative'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/method.rb:320:in `[]'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/method.rb:320:in `do_send'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/analyzer.rb:2383:in `block (2 levels) in do_send'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/utils.rb:101:in `each_key'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/utils.rb:101:in `each'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/analyzer.rb:2382:in `block in do_send'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/type.rb:90:in `each_child'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/analyzer.rb:2358:in `do_send'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/analyzer.rb:1503:in `step'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/analyzer.rb:1050:in `type_profile'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/lib/typeprof/config.rb:123:in `analyze'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/gems/typeprof-0.20.2/exe/typeprof:9:in `<top (required)>'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/bin/typeprof:25:in `load'
from /home/xxx/projects/rbswiki/vendor/bundle/ruby/3.2.0/bin/typeprof:25:in `<top (required)>'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli/exec.rb:58:in `load'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli/exec.rb:58:in `kernel_load'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli/exec.rb:23:in `run'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli.rb:491:in `exec'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/vendor/thor/lib/thor/command.rb:27:in `run'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/vendor/thor/lib/thor/invocation.rb:127:in `invoke_command'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/vendor/thor/lib/thor.rb:392:in `dispatch'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli.rb:34:in `dispatch'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/vendor/thor/lib/thor/base.rb:485:in `start'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/cli.rb:28:in `start'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/exe/bundle:45:in `block in <top (required)>'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/lib/bundler/friendly_errors.rb:117:in `with_friendly_errors'
from /home/xxx/.rbenv/versions/3.2.1/lib/ruby/gems/3.2.0/gems/bundler-2.4.6/exe/bundle:33:in `<top (required)>'
from /home/xxx/.rbenv/versions/3.2.1/bin/bundle:25:in `load'
from /home/xxx/.rbenv/versions/3.2.1/bin/bundle:25:in `<main>'
https://github.com/ruby/typeprof/issues/73
原因はTypeprofの古めのバージョン(0.20.4
)にバグが混入していたため。
最新版にアップデートしたら動きました。
Gemfile
を以下のように更新
- gem "typeprof"
+ gem "typeprof" , '~> 0.21.0'
それなりにエディタ上で型解析が行われ、エラーも確認できました。
ただ、数点気になることがあり、結局Rubyを使うことはありませんでした。
最近のIDEだとカーソルを変数に合わせるとその変数の型がわかったりしますが、そのような機能がないため今どのようなデータを触っているか把握しずらいです。
個人的にはデータ構造がわかることが、静的型解析のいいところの一つと考えているので、その点が微妙だなと思ってしまいました。
これは型注釈を書かないこととトレードオフですが、渡す引数によっては想定していない型も受けてしまいます。
例えば、以下のようなコードの時
# def foo: (Integer | String x, Integer | String y) -> (Integer | String)
def foo(x, y)
(x + y).to_s()
end
# パターン1
p foo(1, 2) #=> "3"
# パターン2
p foo("1", "2") #=> "12"
# パターン3
p foo("1", 2) #=> `+': no implicit conversion of Integer into String (TypeError)
この場合、foo()
は数字しか許容したくなかったときでも、文字列が入ってしまってもエラーになりません。
またパターン3の場合、型的には問題ないですが実行時エラーになってしまいます。
これは受け入れるしかないですが、依存ライブラリが増えて、Untyped
が多くなった時に問題になりそうです。
きちんとテストコードを書けば避けられる問題でもありますが、気楽に書きたいという個人的欲求と真逆になってしまいます。
いったんは Deno+TypeScriptでスクリプトの欲求は満たしつつ、Ruby3の型解析はウォッチを続けようと思います。
基本的に私の思想と相性悪いだけで、Rubyで型解析は型注釈なしの夢のツールになりえると思います。