Kaigi on Rails 2023に登壇した。

kaigionrails.org

2日目の最初だったので、オープニング担当だなと勝手に解釈した。

多分100人ぐらいはいた?50人くらいかもしれない。分からないけど1人でもお客さんがいるならライブをやるとdir en greyの京も言ってたしがんばった。

客観的にどういう公演になったかは分からないけど、主観的には出し切れてスッキリした気持ち。いいライブができた。

もうちょっとネタを盛り込みたかったけど社名を背負っているので自重した。「LUUPで来た」ぐらいは言いっとけばよかった。

周りに与えた影響として一番大きいのはrakeのリリースを急かすことに成功したことだと思う。rakeは影響力が大きいgemなので大変だとは思います。リリースお疲れ様です。

これからはとりあえずプロダクト側の改善を続けて、RubyKaigi 2024に向けてのネタを探したい。

関係者の皆様お疲れ様でした。

rakeのエラー表示がちょっとだけ便利になりました。

rake v13.1.0がリリースされました 🎉

https://rubygems.org/gems/rake/versions/13.1.0

このリリースには私が実装した改善が含まれているので紹介します。 というか書かないと環境変数とか誰にも気づかれなさそう。

Support `#detailed_message` when task failed by ksss · Pull Request #486 · ruby/rake · GitHub

rake task実行時に何らかのエラーによって失敗したとき、error_highlightやdid_you_meanが効くようになりました。

before

$ bundle exec rake demo
rake aborted!
NoMethodError: undefined method `opject_id' for main:Object

after

$ bundle exec rake demo
rake aborted!
NoMethodError: undefined method `opject_id' for main:Object (NoMethodError)

  p self.opject_id
        ^^^^^^^^^^
Did you mean?  object_id

これまでは発生したエラークラスの#messageメソッドを表示していたのですが、#detailed_messageというメソッドが呼べるならこっちを呼び出すようになります。

ruby v3.2.0からException#detailed_messageが導入されており、これを利用しています。

エラークラスに#detailed_messageメソッドさえあればv3.1以前のrubyでも動くっちゃ動くのですが、稀なケースだと思われます。

rbsはv.3.1でも#detailed_messageを呼べるようにしたのでこの施策は有効です。

Debug at stop when task fail by ksss · Pull Request #489 · ruby/rake · GitHub

環境変数RAKE_DEBUGに何かしらの値が入っていると、rake taskが失敗したときにdebug gemによるデバッガーが起動します。

$ RAKE_DEBUG=1 bundle exec rake demo
undefined local variable or method `aaa' for main:Object
[30, 36] in ~/src/github.com/ksss/rake/Rakefile
    30| end
    31|
    32| task default: :test
    33|
    34| task :demo do
=>  35|   aaa
    36| end
=>#0 block in <top (required)> at ~/src/github.com/ksss/rake/Rakefile:35
  #1    [C] Kernel.load at ~/.rbenv/versions/3.2.1/lib/ruby/3.2.0/bundler/cli/exec.rb:58
  # and 14 frames (use `bt' command for all frames)
(rdbg:postmortem)

これまでrdbgコマンドでrakeタスクに対してブレイクポイントを仕込む事はできましたが、エラー発生時にデバッガーを起動するpostmortem機能を使っても最後に起きたexceptionに対してデバッガーが起動するので、本来調べたかったコンテキストが失われた状態になっていました。

そこで rspec-debug gemをヒントにRake::Task#execute実行中に発生したエラーに対してpostmortemを起動することで、本来得たかったコンテキストを得ることができるようになりました。

もちろんdebug gemが使える状況でなければ環境変数をつけても起動しないのでご注意ください。

なにか問題があれば直すので教えて下さい。

まとめ

rake便利。

37歳Web系ソフトウェアエンジニアの転職活動ふりかえり

2023年4月中ごろから6月の今日までの2ヶ月と少しかけた転職活動が終了したので、記録ついでに振り返りたいと思う。

あくまで個人的な記録である。

応募手法

応募方法は、さまざまな方向から行った。

  • Twitterでの公開募集
  • エージェント経由
  • YOUTRUST経由
  • 直接応募

Twitterでの公開募集

正直なところ、一回やってみたかったという部分が大きい。今回の転職活動における大きなチャレンジだった。ありがたいことに20社以上から声をかけていただいた。知り合いのフリーランスの方から「うちが関わってるところどうですか?」という声がけも3名からあった。その節はありがとうございました。

数は多いものの、話を聞く聞かないを考えなくてはならなくなり対応に追われた。公開募集とは、受動的な方法なのだと痛感した。また「会社名も書いてないから怪しいな?」と思ってDMの送信主を調べたら国際指名手配者だったという話はいつまでもしていこうと思う。

かなり終盤で見た記事だが、こちらが参考になった。

転職意思をオープンにした転職活動をする場合におすすめの公開レジュメの活用 - Tbpgr Blog

エージェント経由

エージェントは主にFindyさんにお世話になった。若干動線がわかりにくいが、顧客インタビューの文脈からはじまってエージェント業務をされているらしいので、Findyを通していない応募も気にして応援してくれる。Findyを使わなかったら、かなり応募数も減っていたと思う。悩み事の話し相手や、話すことで考えるタイプの人向けの壁打ちもやってくれるので非常に体験が良かった。

他のエージェントも使っていたが、面談は一度きりで提示される企業も微妙なのばかり。いざ一つ受けてみたら連絡がエージェント経由になっているのに一週間以上連絡がつかなかったり、申請から1ヶ月くらいかかってやっとカジュアル面談できたと思ったらエージェントと言ってることがまるで違っていたりと散々だった。 合うエージェント探しも重要なように思う。

YOUTRUST経由

あまり繋がりがなく、1件カジュアル面談につながったのみだった。

直接応募

1社だけ社のホームページから直接応募した。

話を聞いた数

  • 全面談数: 34回
  • カジュアル面談: 16回
  • 面接: 9回
  • エージェント面談: 4回
  • その他の面談: 3回
  • オファー面談: 2回

全面談数: 34回

期間は約2ヶ月とちょっと。 面談は一日0回〜3回とまちまちだった。GWやRubyKaigiを挟んだりもしているが、何も予定がない日も企業研究や面談の予定取り付け、コーディングテストなどをしていた。

初めの方は余裕ぶって映画館に行ったりしていたが、後半はかなり焦って希望条件を下げてでも種まきをしていた。

カジュアル面談: 16回

もう少し多くても良かったかもしれないが、カジュアルとはいえ人と話すとある程度疲れるのでこれぐらいがちょうどいいのかもしれない。1対1での会社説明は若干コスト高そうで遠慮していた部分もあるが、会社としては知ってもらうだけでもプラスなので遠慮しなくても良かったかも。

面接: 9回

数年以内に転職活動をしていたので、その時の経験を使いまわして話した。もちろん最近の話だったりかなり前の話だったりもするが、話せる話題を職務経歴書やブログエントリーに書いているのでその内容を話す感じ。

技術面接ということで、通話しつつどういうコードを書くか画面をシェアしながら話す面接もあったが、普段の仕事をしている感覚が確認もできるので、緊張はしたけど良い点も大いにあった。

エージェント面談: 4回

最初にお願いしたエージェントは最初に1度話した後、emailでのやりとりになり次第にフェードアウトした。多分、見込みがないから見捨てられたんだと思う。

早々に見捨てられたと割り切ってFindyさんに切り替えた。Findyさんでは定期的にメッセージやミーティングで進捗確認してくれたので、並走してくれている感があって心強かった。転職活動は結構孤独な戦いなので、特有の悩みを話せる存在は大変ありがたかった。

その他の面談: 3回

Twitterで公開募集した際、個人的に話を聞いてくれた某K氏に感謝します。ほとんどエージェント業では?相談料とった方がいいのでは?と思えるくらい様々な有用情報を提供してくださいました。

某N氏もランチがてら話を聞いてくれて自分にない視点をくれてありがたかったです。 感謝。

オファー面談: 2回

現地に行ってオフィスを見学して年収いくらいくらでどうでしょうという話を2社続けて行った。これまでの面接は自分を売り込む営業活動という側面があったけど、オファー面談は毛色が違って、独特の空気感があった。

内定は多いに越したことはないだろうと思っていたが、多いと悩むものは悩む。幸せな悩みだった。

選考落ち

5社選考に落ちた。全て1次面接での選考落ちだった。

理由はいろいろ考えられるので少し深掘りしたい。

得意領域のアンマッチ

ソフトウェアエンジニアと言っても、さまざまな得意領域がある。 DDDなどの設計を得意とする人、組み込みなどの低レイヤーを得意とする人、人と技術全体をまとめるのが得意な人。

僧侶が欲しいパーティには、どんなにいい武闘家だったとしても採用はできない。 洗濯乾燥機を買ったばかりのおうちは、しばらく洗濯乾燥機を買わない。

そんなどうでもいい例えばかり思い付いては自分を納得させるために言い聞かせて精神を保っていた。

レベルのアンマッチ

採用には予算があり、計画がある。どんなに素晴らしい人であっても、ジュニアの椅子にシニアは座れない。逆もまた然り。

文化のアンマッチ

10社以上話を聞いてみて、やはり文化の違いは感じた。これは言葉にするのは非常に難しいのでなんとも言えないが、大事にするものの優先順位だったり、話している時の雰囲気だったりはもちろん個人差はあるものの、会社のカラーみたいなものを感じることが多かった。ここが合わないとそもそも選考に進もうと思わないし、たとえ無理やり選考に進んでも即選考落ちとなっていた。

使用ツール

Hatena Blog

主にTwitterでの公開募集用に、大体自分はこういう人ですよという文章を書いて公開した。 職務経歴書ほど細かくは知られたくないけど、ここまでぐらいならいいかなーという情報を載せておくのに便利だった。

7月から無職なので求職します - スペクトラム

Google Docs

職務経歴書は、以前の転職時に独自に作ったものがあったので、これに付け足す形で流用した。 URLさえ知っていれば誰でも見れる形にして公開している。もちろん見られてもいい情報しか載せていない。 面接時に便利だったが、公開までしなくてもいいと思う。PDFを生成できるので、PDF化してから送るのでも全税良いと思う。 おそらくだが履歴書は意外と厳格に必要なようで、法律で決まっていたりするのかなと想像する。住所や電話番号などの個人情報となるので、これは公開設定はせずに毎回PDF化して送付していた。企業によって要求タイミングはまちまちだった。オーファ時には少なくとも必要っぽい。

Findy

エージェントがFindyだったので、自然とFindyを見ることが多くなり、ひたすらFindyからのいいねをみて受け会社を選んでいた。 期間を切っていいねの検索などはできないっぽいが、「受け取ったいいね数 123」と表示されている。 プレミアムスカウトなる機能があるっぽく、最初受け取った時は公式にドキュメントがないので「ナニコレ?」と混乱したが、エージェントさんによると文字通りプレミアムなスカウトらしい。多いのか少ないのかはわからないが、今回は全部で6件きた。

Spir

spirはスケジュール管理でお世話になった。複数社同時に面談の日程をつめる必要があるので重宝した。Googleカレンダーと連携しているので、Googleカレンダーを使っている人はいいが、私は毎日TimeTreeを見る習慣があったので、見逃し防止のためにはTimeTreeに転記する必要があるのが若干面倒ではあった。

まとめのポエム

たくさん話を聞いていると、「ここでもあそこでも困っている人がいる。手を貸してあげたいけど手は1つしかない……。」と思うことがよくあった。業界の人材不足ってやつでしょうかね。 問題は無限にあるのに貸せる手は1つなので、何に手を貸すかが重要になってくる。これがまた難しい。。。

応募したにせよしなかったにせよ、求人の数だけ困り事があるということは痛感し、その中で自分がやることを決めることの重要性も実感した。 「自分じゃなくてもいいな」と思ったら応募してないし「自分じゃなきゃ」と思える仕事をしたい。 しかしながら自分じゃなくてもいいけどなんとかなってほしい問題も無数にあったのでみんな頑張ってほしい。みんながんばれ!

7月から無職なので求職します

(6/19現在)内定承諾したので現在は募集していません。

過去のアーカイブとしてこの記事は残していますが、募集はしていません。

プロフィール

やってきたお仕事

等など、Web系のサーバーサイド開発を中心に10年以上の経験があります。 全て自社開発サービスです。

得意分野

短期的に素早くマルチタスクに動くことより、長期的に深くシングルタスクで考えるほうが得意です。

技術的な先行調査や、開発環境改善、技術的に高度な課題解決、サービス共通基盤の開発等を担当することが多いです。

技術特化タイプです。

特にOSSへのアプローチ経験が豊富です。

プログラミング言語

Rubyが好きで、8年以上の経験があります。

プログラミング自体が好きで、言語自体の仕組みやエコシステムに強く興味があります。

RubyKaigi登壇など、外部発信も行っています。

OSS活動

ほとんど趣味としてOSS活動を続けています。

最近だとRBS関係が多いです。

これまでに400以上のPRを自分が管理していないリポジトリでmergeされています。merged PR一覧

詳細はhttps://github.com/ksssにまとめています。

希望する企業

得意分野から、企業のステージとしてはアーリーステージよりもある程度成熟したステージの企業の方が合うのかなと考えています。

また、社会基盤的な業界(医療・教育・福祉等)に特に興味があります。

しかし年収次第ではげふんげふん……。

働き方

フルリモート希望です。

働く時間は9時〜18時ぐらいです。

仕事より家族優先です。子供関係で有休以上に休むこともあります。

ブログ

連絡方法

@_ksss_へDMください。

返信を約束するものではありません。

知り合いを優先します。

テストを実行してRubyの型情報を集めるやつを作った

イントロダクション

「テストを走らせて型情報を収集すればいいんじゃない?」そのアイデア自体は話題に上がることが多かったかと思われますが、観測範囲では前例がないように見えます。そこで、実際に作ってこそ見える世界があると思い動くものを実装してみました。

Orthoses::Trace

github.com

orthosesはRBSを生成するための機能を作るフレームワークで、この機能の一つとしてOrthoses::Traceというミドルウェアを実装しました。

例題として、rack-testというgemのRBSを生成したいとします。 その場合の生成コードをOrthoses::Traceを使って以下のように準備します。

https://github.com/ksss/orthoses/blob/db80d506c5fb02dadaa0ae303e0761ba0a543f6f/examples/rack-test/generate.rb

require 'pathname'
require 'orthoses'
require 'fileutils'

include FileUtils

out = Pathname('out')
out.rmtree rescue nil
Orthoses.logger.level = :warn
Orthoses::Builder.new do
  use Orthoses::CreateFileByName,
    base_dir: out.to_s
  use Orthoses::Filter do |name, content|
    name.start_with?("Rack::Test")
  end
  use Orthoses::Trace,
    patterns: ['Rack::Test*']
  run ->(){
    cd "src" do
      load "spec/all.rb"
      Minitest.run
      # avoid to run on at_exit
      module ::Minitest
        def self.run args = []
          0
        end
      end
    end
  }
end.call

このコードを実行すると、rack-testのリポジトリ内のテストコードを走らせ、RBSを生成することができます。

Orthoses::CreateFileByNameミドルウェアの働きによってclass毎にファイルができます。

その一部を見てみましょう。Rack::Test::UploadedFileの場合は以下のような出力を得ることができます。

class Rack::Test::UploadedFile
  attr_reader tempfile: Tempfile?
  attr_reader original_filename: String
  attr_accessor content_type: String
  def self.finalize: (Tempfile file) -> Proc
  private def initialize_from_file_path: (String path) -> nil
  private def initialize: (String content, ?String content_type, ?bool binary, ?original_filename: nil) -> void
                        | (StringIO content, ?String content_type, ?bool binary, ?original_filename: String) -> void
                        | (String content, ?String content_type, ?bool binary, ?original_filename: String) -> void
                        | (StringIO content, ?String content_type, ?bool binary, ?original_filename: nil) -> void
  private def respond_to_missing?: (Symbol method_name, ?bool include_private) -> bool
  def method_missing: (Symbol method_name, *Array[untyped] args) ?{ (*untyped) -> untyped } -> (Integer | String)
                    | (Symbol method_name, *Array[Encoding] args) ?{ (*untyped) -> untyped } -> File
  def append_to: (String buffer) -> nil
  def self.actually_finalize: (Tempfile file) -> bool
  private def initialize_from_stringio: (StringIO stringio) -> StringIO
  def path: () -> String
end

テストコードを走らせて、わりとそれっぽいRBSを得ることができました。

実装方法

TracePointを使いました。基本的にはTracePointのcallイベントで引数の型を、returnイベントで返り値の型を、それぞれテスト実行時に集めておくという発想です。

コード例で説明します。

def foo(a)
  a.id
end

みたいなコードがあったときに、このままではa.idが何を返すのかわかりません。 そこで、実際にコードを実行し、fooが呼ばれたときのaの値を見て引数の型を、返り値を見て返り値の型をfooの型として蓄積しておきます。

結果として

  • 引数aはRecord型, 返り値はIntegerだった。
  • 引数aはRecord型, 返り値はnilだった。

という情報を集めることができます。

これらの情報を統合して

def foo: (Record) -> Integer?

というRBSに落とし込む、というのが基本的な発想です。 コードの実行が必要ではありますが、コードを実行する最高のサンプルとしてテストコードを使用すれば、既存の資産を流用しつつ0からよりも遥かに効率的にRBSを記述することが期待できます。

既存の手法

静的な型ファイルを自動生成する既存の手法としては以下の前例があります。 これらの前例と今回の手法を比較し、どのようなメリットがあるのかを考えます。

rbs prototype

その名の通り、RBSのプロトタイプを作成するためのコマンドです。 静的解析と動的解析の両方があり、コマンドひとつで生成できるので、RBSのプロトタイプ生成として最も有名な方法だと思われます。

typeprof

静的解析によってできるだけ型情報を収集する手法です。かなり精度も高く「そんなことまで分かるのか!」と驚きます。

rbs-dynamic

RubyKaigi2022で発表されたツールで、コードを実行して型情報を得るものです。 今回実施した手法にかなり近いのですが、今回のように既存のテストコードをまるごと回す用途ではエラーが発生し動かせなかったので今回は使用していません。

比較

rbs prototype rb

ソースコードに対して実行します。簡略化のためコメントは除きます。

$ rbs --version
rbs 3.0.4

$ rbs prototype rb src/lib/rack/test/uploaded_file.rb | grep -v '#'
module Rack
  module Test
    class UploadedFile
      attr_reader original_filename: untyped

      attr_reader tempfile: untyped

      attr_accessor content_type: untyped

      def initialize: (untyped content, ?::String content_type, ?bool binary, ?original_filename: untyped?) -> void

      def path: () -> untyped

      alias local_path path

      def method_missing: (untyped method_name, *untyped args) { () -> untyped } -> untyped

      def append_to: (untyped buffer) -> nil

      def respond_to_missing?: (untyped method_name, ?bool include_private) -> untyped

      def self.finalize: (untyped file) -> untyped

      def self.actually_finalize: (untyped file) -> untyped

      private

      def initialize_from_stringio: (untyped stringio) -> untyped

      def initialize_from_file_path: (untyped path) -> untyped
    end
  end
end

rbs prototype runtime

$ rbs prototype runtime -r rack/test/uploaded_file 'Rack::Test::UploadedFile'

rbs prototype rbとほぼ変わらない結果となったので割愛

typeprof

$ typeprof --version
typeprof 0.21.7

$ typeprof src/lib/rack/test.rb src/spec/rack/test/uploaded_file_spec.rb
# ...snip...
class UploadedFile
  attr_reader original_filename: String?
  attr_reader tempfile: String | StringIO
  attr_accessor content_type: String
  def initialize: (String | StringIO content, ?String content_type, ?bool binary, ?original_filename: String?) -> void
  def path: -> untyped
  alias local_path path
  def method_missing: (:must_respond_to | :pos | :read method_name, *Symbol args) -> untyped
  def append_to: (untyped buffer) -> nil
  def respond_to_missing?: (untyped method_name, ?false include_private) -> true
  def self.finalize: (String | StringIO file) -> ^-> untyped
  def self.actually_finalize: (untyped file) -> untyped

  private
  def initialize_from_stringio: (String | StringIO stringio) -> (String | StringIO)
  def initialize_from_file_path: (String | StringIO path) -> void
end
# ...snip...

比較考察

出力は一部しか示していませんが、全体的な比較として以下のようになりました。

Tool メリット デメリット 備考
rbs prototype 実行が非常に手軽だった ほとんどがuntypedだった なし
typeprof 実行が手軽、かつある程度型がある。インスタンス変数も対応している。 untypedはある rbs collectionで型情報を追加できる可能性がある
Orthoses::Trace 型情報が多い 実行のためにコードを書く必要がある rbs collectionで型情報を追加できる可能性がある

手軽さと正確性にある程度のトレード・オフの関係があるように見えます。

もう少し細かく比較してみましょう。

実行の手軽さ

圧倒的にrbs prototypeやtypeprofが手軽です。Orthoses::Traceではちょっとしたスクリプトレベルのコードを書く必要があり手軽とは言えません。

型の正確性

方法によって考慮されている型情報に差はありますが、どの手法でも最終的な型情報はプログラマーの理想のものになるはずです。よっていずれの手法でもある程度の手直しは必要そうです。

対応範囲

typeprofは定数やインスタンス変数も対応しているのに対して、Orthoses::TraceではTracePoint軸なのでフックが設定でず対応できません。typeprofすごい。 また、Orthoses::Traceでは、呼ばれていないメソッドは情報がないので型生成できません。typeprofは呼ばれてなくても類推できます。typeprofすごい。

テストツールごとの対応

Orthoses::Traceではコードを書くことでテストを走らせ、型情報を得ることができることがわかりました。 テストツールは様々にあり、minitestやtest-unit、rspecなどが有名です。コードを書く以上、それぞれのツールに対応したコードを書く必要があります。 ここではOrthoses::Traceを使った場合のテストツール毎のコードの書き方を示しておきます。

まずテストコードを含むソースコード全体が必要なので、ソースコードgit clone等でダウンロードしておきます。 以後説明のため、このソースコード置き場のディレクトリー名を仮にsrcとします。

minitest

最初に示したrack-testがminitestを使っています。 minitestを使っている場合、autorunを使っていなければload "test/run.rb"みたいな感じでテストコードを読み、Minitest.runでテストを走らせることができます。しかし、大抵はautorunを使っているので工夫が必要です。 autorunはat_exitで実行されるので、そのタイミングだとorthosesで解析するProcの外になってしまいます。 なのでProc内でテストを実行しつつ、autorunを消すためにメソッドをまるごと上書きしています。Minitest.runはexit codeを返すインターフェースなので0を返すのがミソです。

test-unit

多分こんな感じだと思います。

Orthoses::Builder.new do
  use Orthoses::Trace, patterns: ['Foo*']
  run ->(){
    cd "src" do
      load "test/foo_test.rb"
      Test::Unit::AutoRunner.run

      class ::Test::Unit::AutoRunner
        def self.run(*, **)
          0
        end
      end
    end
  }
end

rspec

あんまりrspecを使ったライブラリーを見つけられなかったのですが、おそらく以下で動くと思います。 ただし、テストが成功しないとexitで強制終了してしまうのですが、テストが失敗しているということは型は間違っている可能性が高いのでこれでいいのかも。

require "rspec/core"
Orthoses::Builder.new do
  use Orthoses::Trace, patterns: ['Foo*']
  run ->(){
    cd "src" do
      load "spec/foo.rb"
      RSpec::Core::Runner.invoke
    end
  }
end

めんどい

テストライブラリーに合わせて柔軟に対応できる分、自分でコードを書かなければいけないのがめんどいので、もう少しサポート方法を考えたいところですね。。。 しかしながらリポジトリ毎にテストの走らせ方は千差万別で、gemの依存関係もそれぞれ異なってくるので統一された方法 は難しいと考えています。

思うところ

TracePoint#enable(target: )問題

attr_accessor系のメソッドがRubyVM::InstructionSequence.ofに対応してくれるともう少しコードが簡単になるのですがレアケースそう……。

実装の詳細

実装面はかなり端折りましたが、実は面白い工夫が盛りだくさんです。TracePointに興味がある人は覗いてみてください。

typeprofすごい

ということでOrthoses内でtypeprofを呼ぶことができる拡張も作りました。

https://github.com/ksss/orthoses-typeprof

これまでにOrthoses内で蓄積した情報をtypeprofに渡して型の参考値にすることができます。

ちなみに今回の出力結果は以下にまとめたので比較してみてください。

Orthoses::Trace: https://github.com/ksss/orthoses/tree/main/examples/rack-test/out/rack typeprof: https://github.com/ksss/orthoses-typeprof/tree/main/examples/rack-test/out/rack

感想

これでライブラリーのRBS追加が少し楽になるかも……?どうだろう、typeprofで十分なのかもしれない……。

とりあえず作ってみる事はできたので、今後は使ってみて便利かどうか確かめる。ですかねえ……。

そもそも何かしらやりたいことがあって、そのときに思いついた手法なのですが、思いついて実装してブログを書くのに5ヶ月ぐらいかかってしまい本来の目的を見失いました。お後がよろしいようで。

思いついたとき。

YARDタグからRBSを生成する

YARD

YARDはドキュメンテーションツールです。 Rubyのコメントに

# @param [String] a
# @return [void]
def foo(a)
end

みたいな記述を見たことがありませんか? この@paramがYARDのタグ名、[String]がタグのもつ型情報です。aは引数の名前ですね。

YARDではこの型情報を元にリンクを貼ったwebページを生成したりできます。

rubydoc.infoがまさにYARDによって生成されていますね。

RBS

RBSRuby公式の型を記述する書式、およびツールの名前です。

こんなかんじです。

def foo: (String a) -> void

YARD to RBS

では

# @param [String] a
# @return [void]
def foo(a)
end

とかかれたソースコードを読み込んで

def foo: (String a) -> void

とかかれたRBSを出力できたら便利だと思いませんか?

orthoses-yard

というわけで実装したのがorthoses-yardです。

github.com

orthoses-yardはorthosesの拡張です。 orthosesが何かは、RubyKaigi2022で紹介した資料を御覧ください。動画はもうすぐ出ると思います。*1

orthoses-yardではこのorthosesの仕組みに乗っかって、任意のpathのRubyをパースしたYARD情報からRBSを生成します。 「アプリケーションの一部だけしかYARDドキュメント書けてないんだよな」という場合でも柔軟に対応できます。

基本的には@paramタグを引数として、@returnタグを返り値として解釈し、メソッドの型定義を生成しています。 また、@yieldparamタグと@yieldreturnタグにも対応し、ブロックの型表現も可能です。

これ(Rubyコード)から

class Foo
  # @param [String] a
  # @return [Integer]
  # @yieldparam [Symbol] b
  # @yieldreturn [Boolean]
  def foo(a)
  end
end

これ(RBS)を作れます。

class Foo
  # @param [String] a
  # @return [Integer]
  # @yieldparam [Symbol] b
  # @yieldreturn [Boolean]
  def foo(String a) { (Symbol b) -> bool } -> Integer
end

orthosesは柔軟なので、アプリケーションだけでなくgemのRBSもorthoses-yardを使って生成できます。手始めにyard gem自体の型定義をYARDタグを元に生成してみました。yard gemならYARD記法を使いこなして広いパターンでの挙動確認ができそうだったのが選択理由です。

Add Yard gem by ksss · Pull Request #217 · ruby/gem_rbs_collection · GitHub

既存のYARDドキュメントを元にメソッドや定数の型を決定することができています。

RBS自動生成の肝

RBS自動生成最大の問題はmethodの型定義です。

orthosesによるRBS自動生成では、classの名前や継承関係、includeしているmoduleを自動的に洗い出したりするのは得意ですが、 methodの型定義を決めることは困難なのです。

なぜならmethodの型は基本的には人間が決めるべきであり、人間が書くべきです。 正しいものが何なのかわからないと、methodの実装自体が型チェックができないですしね。

def foo
  if cond
    "foo"
  else
    :bar
  end
end

上記例だと静的解析すればfoo: () -> String | Symbolとなりますが、 実際はStringだけを期待していてSymbolが返るのは誤りになるケースも多くあるでしょう。 ましてやattr_accessorだとさらに難しくなります。

methodの型は手で書くしか無いのでしょうか……。

でも自動化したいですよね。

もし既存の資産であるYARDドキュメントを利用できれば、methodやattr_accessorであっても型ファイルを生成できます。

このYARDからの生成によって、このmethod定義問題に良いアプローチができるんじゃないかと期待しています。

YARDを書けばRBSを生成できるならYARDを書くモチベーションを上げることもできますし、YARDとRBSで二重管理になることも防げます。

補足

ちなみにRubyの型界隈に詳しい人は、似たようなツールとしてsordを思い浮かべたと思います。 確かに今回実装したものはsordによく似ていますが、orthosesの拡張なので他のorthosesの機能と組み合わせられるという点が大きなウリとなっています。 例えば「Railsプロジェクトで一部だけYARDを使っている」という場合でも、orthoses-railsrails拡張と組み合わせてプロジェクト全体の型定義をコントロール可能です。

YARDの限界

YARDは非常に歴史あるプロダクトであるため、途中から追加されたkeyword argumentsの対応が弱いように感じます。YARDタグだけではそれがキーワード引数なのかどうか判別できません。もちろんYARD的には問題ないのですが、型定義を生成する場合は情報が足りません。

# どっちも同じ書き方

# @param [String] key
def bar(key)
end

# @param [String] key
def foo(key:)
end

orthoses-yardではMethodオブジェクトからparametersをとってYARD記法とマッチングさせることによってこの問題を解決しています。*2 また、RBSでのoverloadが表現できないことも若干ネックかなと思います。

# YARDだと表現できない
def foo: (Integer) -> Integer
       | (String) -> String

これから

大まかな機能はできましたが、まだまだ細かいTODOが残っているので機能を拡充してプロダクトで使ってみて更に磨きをかけていこうかと思います。

*1:動画では「orthoses-yardが欲しいんだけど誰か作らない?」と言ってますが自分で作ってしまいました。プログラミング楽しい。

*2:引数を移譲するメソッドではこの方法では対応できないので別途対応を考え中

RubyKaigi2022で発表した

rubykaigi.org

speakerdeck.com

RubyKaigi2022で発表した。オンライン参加で、発表は録画だったので、発表したかどうかは奇妙な感じだけど、ともかくできるだけのことはやりきった。

僕にとって、RubyKaigiは2013が最初の参加だった。以来「いつかは発表を……。」と思ってはいたけど、ネタがなかったり、CFPを出したけどRejectだったりだった。

今回、"Rubyに関する国内最大級の国際会議"に初登壇できたのは万感の思いだ。本当に嬉しい。

一方、採用率(採用数 / CFP数)は実は2倍もない。つまり誰にでもチャンスはあって、やれる覚悟があるかどうかが重要なのだ。みんなも発表やってみよ。

KPI

"GitHubリポジトリのスター100個"を目標にしてたけど、結果としては30くらいだった。

https://github.com/ksss/orthoses

やはり何をするものか伝わらなかったり、どうやって使うか分からないから試せないなど、色々と足りないと思うので目標に向かってがんばっていきたい。

トークの反応

良かったのか悪かったのかはよくわからない。なにせ何人の人が会場の席に座ってたのか分からないし、何人がオンラインで観ていただけていたのかも分からない。

Twitterの反応はそこそこいただけたと思う。本当に嬉しかった。Twitterに書いていただいた感想は最後に乗せるが、良さげな反応をいただいて光栄だった。

トーク内容

技術的な詳細(TracePoint系やRBSのマージ系)はRubyKaigiっぽいと思いつつも省略した。実装はコード見て。

代わりにミドルウェアの仕組みや書き方を多めに解説し、エコシステムを作る目標によせた。発表は宣伝効果が高いはずなのでこれを狙った。

実は伝えたかったこと

エコシステムを作りたい

自分にしては大きな規模の構想なので、エコシステムを作って人を巻き込んでより大きなことをやってみたい。モデルとしてはfluentdだ。あんな感じでコミュニティで拡張gemを作ったり使ったりするようになったらいいなあ。

gem_rbs_collection問題

railsアプリケーションの型付けという目標を達成するためには、既存のrails gem用の型では不十分で、大きく直さなければならない。

また、gem_rbs_collectionのメンテナンスコストを下げることも目標の一つなので、orthosesでrails gem用の型を生成する運用を狙って導入PRも作ったが、反応がない。ちょっとやり方が唐突かつ差分が大きかったのが原因かと思うので、これを改善してなんとか導入に繋げたい。

オンライン参加

直近、家族の病気が続いているのもあって間違った選択だったとは思っていない。

思ってはいないけどインパーソンが羨ましい気持ちは十分にある。せっかく"発表者"という話しかけられやすい状況なのに交流ができない。ノベルティは何一つもらえないので、物で思い出が残らない。。。

伊勢海老カレーはいただけたのが救いだった。味も美味しかったけど、作るのも楽しかった。ESM様ありがとうございます。

録画配信

壇上発表ではなく録画配信は初めてだったけど、これはこれでメリットがあるように思った。 トーク以外に力が割ける。

視聴者様の理解に少しでもつながればと事前の予習記事を用意してスライドも当日朝に公開できた。 トーク中もTwitterを覗いてlikeをつけてまわった。質問ぽいのはリプライもした。自分のトークを観てくれて反応までしてくれているのは嬉しい。尊い。 「ちなみに」みたいな補足情報を投げれるのも録画配信のおかげだった。

体調を考慮しなくてもいいので確実に発表できるし、録音もちょっとずつ撮ってそれぞれをつなげるYouTuber方式ができた。

録画方法

録画はKeynoteiMovieを使った。Keynoteには録画機能があるので、これで頭からスライドに合わせて声を撮り、かんだりしたらやり直しつつ少しずつ前に進む。完全に声優のアフレコ現場だ。 Keynoteの発表者ノートに話す"セリフ"を完全に書き出す。これが台本だ。 マスクをしてポップノイズを減らし、ゲーミングヘッドセットのマイクで録音する。ノイズを減らすためにクーラーの音が入らない締め切った部屋で撮った。汗だくになって二晩かかった。途中でスライドを足したりしたけど、時間にして6時間くらい。 Keynoteで録画ができたら、iMovieで編集作業。 編集作業は、間を調整したり、クーラーや洗濯機や風の音によるの背景ノイズを除去したり、音レベルを調整したり、咳払いをカットしたり。Keynoteである程度NGとかはなくなっているはずなので一晩で終われた。 最後にmp4動画とKeynoteファイルを提出。keynoteファイルには日本語での"セリフ"が入っているので翻訳しやすくなったはず。

英訳

本番中に英語の放送も気になって見てみたが、「自分の話したことが英訳される」という経験したことのない感動があった。英語版もYouTubeに上がったらいいなあ。

しゃべり

何年も子供に絵本を読み続けて身につけた技能として、「色々な声色で子供にも分かりやすく書いてある文字を読む」というスキルを身に着けていることに気が付き、これを活用したかった。

もっと堂々とした引き込まれるような分かりやすい発声を心がけたが、実力不足でボソボソとしてしまい怪しいセミナーみたいな謎の雰囲気が出たっぽい。

別セッション

自分の動画はイヤというほど観ているので裏番組のSyntax Treeのセッションも観ようと思ったけど、Twitterで思いの外反応をいただけてこちらの反応に忙しくて観れなかった。

スポンサー

僕の表現方法がまずくて、まるでorthosesプロジェクトにGitHub社とWantedly社がスポンサーとして付いているかのような表現になってしまった。 正しくは、2社とも個人のとしてGitHubスポンサーをいただいているだけで、orthosesプロジェクトとは関係ないです。なんなら個人スポンサーがついた理由も分からずにいる。

もしものときのために

もし通信障害が起きたときのためにYouTubeに動画をuploadしてた。こちらは使う機会がなくてよかった。配信チームに感謝。

Railsアプリケーション

やっぱりRailsアプリケーションの型生成例を出したほうがインパクトがあるとは思うので、これはなんとか提示したかったところ。手元では作れているけど、適当なRailsアプリケーションを探したりとかまでできなかった。そのうちやりたい。

雑談

このまま誰とも話さずRubyKaigiを終えるのは辛かったので雑談に加わった。 めちゃ楽しかった。

トーク中のTwitterの反応

さいごにトーク中のTwitterの反応を掲載します。 無許可掲載すみません 🙏