CLOSE

Breaking Change(全2記事)

RuboCopの実装に見る、Rubyの“破壊的変更”との正しい付き合いかた

Rubyはバージョンアップによって、やむなく旧バージョンとの互換性がなくなってしまうことがあります。それが破壊的変更です。後編では、静的コードアナライザーモジュールであるRuboCopのコミッターの@koic氏が、破壊的変更にタイルする実際の実装について紹介します。

Pull Requestは興味をもってもらうように書こう

koic氏:コンテクストとして、なぜ必要かというところをまず書きます。CHANGELOGを辿るまで、まず何をすればいいかわからないと、もうけっこう致命的なんですけど。

実装ポイントとしても次のような話をします。どんなキーワード引数を使えばよいかというところ、それからメリットとして、ユーザーのアップグレードでの痛みは減らせますよといったような話。

そして実例ですよね。「Beforeがいきなりエラーになる。AfterはもうこのAPIは非推奨なので、新しいキーワード、これに置き換えるとよい」と。自分で言うのもなにですが、「非推奨警告かくあるべきみたい」という感じで、キチンとやったんですよ。

コードのdiffとしては、基本的な部分は、こうした旧APIをデフォルト引数で取っています。そのデフォルト引数のところに、何か値が変わって入ってたら、そこで警告を出すみたいな。なので、キーワード引数も、キチンと受け入れるというところですね。

これはRuby 2.5で、ERBのインターフェースが実はちょっと変わっているので、そのへんのところをヒントにした実装なんですが、そうした実装をちょっと入れています。legacy_sourceとして元の引数でも受け入れるようにして、legacy_sourceが渡された場合は警告を表示して、その値を優先して使うというの入れています。それを125API分やるんですよ。

(会場笑)

どういう意味かというと、メソッド名があるじゃないですか。引数名があるじゃないですか。引数の数もあって、デフォルト値もあって、警告文も1個1個違うのを125個ですよ。

たぶん人間には難しいので、セルフレビューする気も起きません。こういうときに僕は、RuboCopという使い慣れたツールを使います。

何をしたかというと、この非推奨警告のパッチを送るにあたって、まずキーワード引数に変わる前のFakerのコミットハッシュを手に入れて、そのFakerの手元のリポジトリをそのバージョンにrevertして、それを今度はFaker 2に移行用のツールに自分のCopを作って、そこにauto-correctするCopを作って、それを適用して送ったんです。

期待していないコードと期待するコードを書く

こうした流れのなかで、auto-correctするCopについて話そうと思います。Copのおさらいを少しすると、RuboCopって、期待していないスタイルのコードを期待しているコードに自動修正する機能が入っています。そうしたCopは、RuboCopが提供しているAPIなどを使えばオリジナルで作れます。

このコードを発掘したんですけど、みなさんお忘れかもしれませんが、Rubyって、オブジェクト指向“スクリプト”言語Rubyです。Webを書くためだけに使われる言語ではないんですよ。

何を言い訳したいかというと、ローカルリポジトリで書きなぐったものを今年の夏ぐらいに発掘したんですが、だいぶスクリプト感いっぱいですね。なんか書きなぐってRubyキメてとりあえずみたいなの。

とりあえず全部出します。量もあったので、どんなことをしたかというところを、ちょっと雰囲気でお伝えしますね。

僕が最初にやるときって、だいたい最初に期待を書くんですよ。「これが悪いコードというか期待していないコードで、これが期待しているコードです」という差分。今回だとこれが差分です。このようになればいい。このあとにテストコードも書いて実装に入っていくんですが、今回は割愛しています。

具体的には、こう実装する

バッドケースを捉える実装例として、今回はメソッドの定義部分、引数の並びの定義のところをやりたかったので、on_defでメソッド定義のdefでひっかけて、そのノードに対する処理を書きました。

RuboCopは、これ親クラスにした中でついてくるadd_offenceにまで呼び出しがあると警告が出るんです。ここまでいったら警告が出るので、警告しない、ガード条件みたいなものを入れているんですね。ここって。

node、まあdefで渡された引数argumentsが全部〇〇unlessなんですが、ここに出てくるkwarg_typeやkwoptarg_typeが、ここにあるdef do_something……です。このruby-parseというコマンドは、parser gemをインストールすると一緒にインストールされます。

RuboCopはparser gemに依存します。そのparser gemはRubyのコードをS式で返すgemなんですが、ここでどういうノードがこのdef do_something(foo:, bar: 1)とすれば返ってくるかというと、こういうS式なんですね。

ここで、「foo:の値なし」と「bar:の値あり」だと、返ってくる値が違うんです。これ最初、僕は片方しか書いていなくて、バグっていました。こういったところは、条件に応じていればキチンとパスすると部分なんですけどね。

実装においては、このようにして警告する対象を見つけるのですが、対象を見つけたら、それを自動修正するためのAPIが別にあります。RuboCopにはautocorrectというメソッドがあって、それをoverrideするとコールバックされるつくりになっています。いればキチンとパスすると部分なんですけどね。

ここでnodeから引っ張ってきているキーワード、すなわち、あるnodeを引っ張ってきて、それの引数が1というところですね。第1引数、第2引数、第3引数と。このへんは、警告の文字列を組み立てるのに使ったりしています。

今度は、長いので何ページかにわたるんですが、下位互換のための変数をこの「legacy_もともとのオリジナルの引数名」で足して組み合わせます。

これはどのクラスのAPIを使ったら警告を出すという処理で、そのクラスの定義を追うのに、Rubyって何重にもネストをかけていますよね。RuboCopでは、僕は再帰処理をよく使うのですが、これも再帰処理を使っていいます。親をたどってクラスノードになるまでひたすらたどるという処理です。たどったらそれを返してclass_nameとして取っています。

これは自動修正コードの部分。ヒアドキュメントですね。ヒアドキュメントの中で置換する文字列を組み立てているわけです。

最後にこのautocorrectメソッドの中。lambdaで書けるんですが、このlambdaでcorrectorというブロック変数をとったオブジェクトに対して、insert_beforeと呼び出しています。何をしているかというと、”#{legacy_argument} = NOT_GIVEN, “が、このパッチの下に書いてある引数を追加した自動修正コードなんです。

こちらが警告条件のところ、conditionのところを足すという、autocorrectが適用されているところなんですよ。

というところで、このかたちで一応ではあるんですがPull Requestを出して、Mergeいただいた。「おマージいただいた」んですよ。

よかったこと、考えたこと、これから考えることを振り返る

でもそれで作っておしまいではないじゃないですか、ソフトウェアって。ユーザーの反応があるわけで、「推奨警告の量が半端なくて大変だったよ」というふうにまとめて言われて、「せやなぁ」っていう。

(会場笑)

フィードバックに対してのKPT(振り返り)として、よかったこと・考えたこと・これから考えることということを、ちょっと僕なりに考えたんです。

よかったこととしては、推奨警告。僕の作ったコードが活用されたって、すごくいいことですよね。書いて終わりじゃないですから。ただよくないのは、やはり125以上の破壊的変更を直撃するとつらそうで、僕がやったわけではないんですが、やはりつらそうだなというところ。

(会場笑)

そこを緩和するのに自動修正可能なツールを提供すれば、アップグレードをより楽にできるかなと。ユーザー側のコードを自動修正できるとよいのではと思って作ったのがrubocop-fakerだったんです。

一芸は開発を加速する。これはRuboCopの開発などで身につけた能力ですが、いろいろほかの知識ともひもづいていて、以前RSpec 2からRSpec 3に移行するときには、Transpecというものがあって、それに似た何かが作れると便利なのかなとか考えました。

あとは、先ほど話した、Fakerに非推奨警告を入れるPull Requestを作る過程で、ついでにこのgemを作ってたいような気がしています。似たようなものなので、うろ覚えなんですが。

やっているのは、こういうgemです。Before・After上のほうにあると、Faker::Avatar.imageで、普通の positional argumentを適用するとキーワード引数に変わるというもの。

uptreamに取り込んでもらって宣伝する

こういうのを作ったんですが、実装としては似たようなものなので、ちょっと1回割愛というかたちにしたいのです。けれどもここで、ちょっと1つポイントがあります。このgemは、僕が作ったんですが、たぶん作っただけだとなんとなく使われないなと思いました。パッチ会でもお世話になっている松田さんの、僕の中の名言なんですけど、「gemは作っただけで勝手に広まるわけではなくて、使われるためのひと工夫が必要」という言葉。これがパッと頭をよぎって、じゃあこれどうするかなと。

少し似たもので、「64パーセント」という数字があります。だいぶ昔の数字ではあるんですが。これはライブラリではなくて、アプリケーションですね。アプリケーションでは、作っても使われない機能が64パーセントあると。gemも似たようなものではないかと。作ったけれども使われないって、けっこうなんかアレなので、こうした課題を自分なりに、作ったあとももっていいます。

ならば、この課題に対して最強の宣伝方法は、upstreamに取り込んでもらうことです。これがFakerの#1724です。

これが何かというと、警告としてauto-correctできるよというものを出す、警告というか説明を出すというものです。でもそれってそもそも僕がオリジナルで考えたものというよりは、factory_botという、gemのアップグレードの際にauto-correctを使うことでRuboCopの移行をスムーズにするものがあり、それにちょっとインスパイアされて作ったものです。

そこで、こうしたかたちで解決策として、そのgemを使うとよいよというところで・・・。Before/Afterですね。

Beforeだと、positional argumentをkeyword argumentに変えましょうという警告だけだったのを、さらにその警告をコピペできるように書いてあります。このRuboCopの、--require rubocop-faker only……そのFaker gemが唯一持ってるAPIです。auto-correctすると移行ができるよというやつです。ユーザーとしてもメリットがあります。

まだ続く破壊的変更への挑戦

心配だった点が、やはりありました。僕が個人で作ったFakerのorganizationではないものへ依存していいのかというところです。そのあたりは、キチンと説明すればMergeしてもらえて。「おマージ」いただきました。

ただこれも、その後がまだあります。Fakerの課題#1692です。「引数1個の場合、kwargsはやりすぎじゃね?」って。まぁ「せやなぁ」という感じなんですが、困ったことに、いやこれにいいねとかいいねとか……「いいね」じゃないんですよ!これ、変えちゃいけないんですよ!

(会場笑)

僕は、Fakerの例えば「2.いくつ」で、またそれをやるんだったら非推奨警告を出して、消すんだったらまたそうやるとか警告を立てないとかいうふうに一応出して…。ここで今止まっているので、続きはGitHubでという感じなのですが、なにかこのあたりに興味ある方、いらっしゃればまた何かコメントいただいて、そのあたりディスカッションできればと思います。

そもそもキーワードかポジショナルか、どちらがよいかというところも、どこまで議論されたかよくわかりません。そうしたところの参加もできると思います。

破壊的変更は避けられない。gemアップデートをこまめにすることで変更範囲が少ないうちに対処しよう

まとめます。破壊的変更というのは、やはり、最初から弱点のない設計にするのは難しいです。悪くしようなんて誰も思っていなくて、よくするために破壊的変更するときがあります。

僕らアプリケーション側としては、やはりいきなり大きく壊れると、いろいろなものに影響し、問題の仕分けがよくわからなくなります。ですから小さくリプレースをするという意味でも、gemのアップデートをこまめにやっていくと、変更範囲も少なく、原因の特定までも早いです。これをわりとやっていくといいのではないかと思います。

そして、そのへんのライブラリ、Railsもライブラリですから、それも含めてバージョンアップするのが非常に重要です。自分や周りは、例えばRails 4のプロジェクトをやりたいとかRails 6のプロジェクトをやりたいとか、プロジェクトの性質にもよると思うのですが、Railsだけで判断すると「どっちですか?」という話があると思うんです。

僕は、この場にいる人の中で古いほうのバージョンを選ぶ人はいないと思って勝手に話していますが、新しいバージョンを使うというのはすごいです。なんて言うんですか、未来にキチンと残して価値になるものなので、そうしたバージョン越えというのは、やっていけるといいと思います。

ふだんから、そのアプリケーションをバージョンアップするというところもそうですし、今話したようにオープンソースというかたちのところに何か関わることによって、もしかすると不必要だったかもしれない破壊的変更を止められるかもしれません。例えば正規表現のmatchのnil渡しのところだとか・・・。そういった破壊的変更でのダメージの緩和、例えば非推奨警告をキチンと入れておきましょう。事前になにかコメントをつけるとか、独自でなにかやるとか。

まずやるべきは非推奨警告の確認から

手元の非推奨警告を見るところは、手始めにやれると思います。そうしたところからやっていきましょう。とくに今回Ruby 2.7で警告が入るRuby 3のキーワード引数の分離などは、かなり大きな変更だと思います。ちょっと先の長い話ですが、2020年末ですから1年後です。みんなでうまくRuby 3へのアップグレード自慢などをしていけるといいと思っています。

最後になりますが、そのコミュニティというところで、今回Rubyコミュニティである「平成Ruby会議」だったわけですが、コミュニティへの参加の仕方も、いろいろ多様にあると思います。

今回はその中の1つにしか過ぎないかもしれませんが、それはとても大きなものです。例えばコードを書いて何かをやるとかコードの世界でディスカッションするといった参加方法です。来年もRubyKaigi 2020があるので、そこでそういったコード、そして自分たちが自慢できるような何かを一緒に話せればよいと思います。

ご清聴ありがとうございました。また来年のRubyKaigiでお会いしましょう。

(会場拍手)

続きを読むには会員登録
(無料)が必要です。

会員登録していただくと、すべての記事が制限なく閲覧でき、
著者フォローや記事の保存機能など、便利な機能がご利用いただけます。

無料会員登録

会員の方はこちら

この記事のスピーカー

  • koic

同じログの記事

コミュニティ情報

Brand Topics

Brand Topics

  • 今までとこれからで、エンジニアに求められる「スキル」の違い AI時代のエンジニアの未来と生存戦略のカギとは

人気の記事

新着イベント

ログミーBusinessに
記事掲載しませんか?

イベント・インタビュー・対談 etc.

“編集しない編集”で、
スピーカーの「意図をそのまま」お届け!