RubyのURI.joinはなぜ直感に反する動きをするのか?
Honeybadgerでは、ついに一つの節目を迎えました。販売ページがメインのRailsアプリケーションから分離されたのです。これは何年も前から私のウィッシュリストに入っていたものの、優先度はそれほど高くありませんでした。
この移行作業の一環として、特定のリダイレクトリンクを作成するためにURI.joinを使う場面がありました。ところがすぐに問題にぶつかります。URI.joinが期待どおりに動作しなかったのです。
私は、複数のパス断片を受け取って、それらを次のように連結してくれると考えていました。
# こうなることを期待していた。しかし現実は違った。
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/plans/change"
しかし実際のjoinメソッドの挙動は、はるかに奇妙なものでした。パス断片の一つ「plans」が消え去り、最後の「change」だけが使われたのです。
# 実際に起きたこと。
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"
一体なぜこんな動作になるのでしょうか?
誤解の正体
実は私は、URI.joinがArray#joinの特殊版のようなものだと考え、URLの断片を組み合わせて完全なURLを作ってくれると期待していたのです。
しかしそれはURI.joinの役目ではありません。大きな誤算でした。
joinメソッドのソースコードを見てみると、単純にすべての引数を順番に処理し、それぞれに対してmergeを呼び出しているだけだとわかります。
# File uri/rfc2396_parser.rb, line 236
def join(*uris)
uris[0] = convert_to_uri(uris[0])
uris.inject :merge
end
mergeメソッドは2つのことを行います。
- "pages"のような文字列を相対URIオブジェクトに変換する。
- その相対URIをベースURIに対して解決しようとする。この処理は、RFC2396のセクション5.2で規定されている方法に厳密に従って行われる。
なるほど、それなら先ほどの予想外の挙動はどう説明できるのでしょうか?
URI.join("https://www.honeybadger.io", "plans", "change")
=> "https://www.honeybadger.io/change"
順を追って見ていきましょう。上記のコードは、実質的に以下と同じです。
URI.parse("https://www.honeybadger.io/plans").merge("change")
このコードは、絶対URI「https://www.honeybadger.io/plans」に対して、相対URI「change」を解決しようとしています。
この解決処理は、RFC2396のセクション5.2.6に従います。同セクションには次のように記されています。
a) ベースURIのパスコンポーネントのうち、最後のセグメント以外をすべてバッファへコピーする。言い換えれば、最後(最も右側)のスラッシュ文字以降の文字があれば、それらは除外される。
b) 参照(リファレンス)のパスコンポーネントをバッファ文字列に追加する。
実際に手順に従ってみましょう。
- 絶対URLの最後のセグメント以外をすべてコピーする。これにより
"https://www.honeybadger.io/"が得られる。 - 相対パスを追加すると、結果は
"https://www.honeybadger.io/change"となる。
これで世界の秩序が取り戻されました!
まとめ
URI.joinは確かに複数のパス断片からURLを構築できますが、それは本来の設計目的ではありません。このメソッドはもう少し複雑なことを意図して作られています。すなわち、RFCで定められた標準に従って、URIを再帰的にマージすることです。
ちなみに私自身の目的——新しい販売ページへのリダイレクト用URLの構築——については、結局Array#joinを使うことにしました。:)
2016年8月12日追記: この記事を公開した後、「この用途にはFile.joinを使えばいい」という提案のツイートをいくつかいただきました。確かにFile.joinを使えばスラッシュの重複(例:/my//path)を避けられますが、Windowsのようにパス区切り文字がフォワードスラッシュではないOSでは動作しなくなるという欠点があります。
-
なぜクラスを作るのか?抽象化の重要性と良いクラス設計の基本
前回の記事「なぜnilを使うのか」に続き、普段当たり前と思って使っているものについても、同じ問いを立ててみるのは良いアイデアではないでしょうか。 例えば… 巨大で醜いコードの塊を1つ作るのではなく、なぜ私たちはクラスを使うのでしょうか? 基本的なレベルで言えば、コードとデータを論理的な単位に整理するためにクラスを使います。しかし、それだけではありません。 クラスを使うと、抽象化(abstraction)を作り出せるのです。 抽象化とは何か? 実は、私たちは毎日、毎分、抽象化を利用しています。 今この記事を書いているキーボードは、次のようなもので構成されています。 ケーブル Cherry MX
-
Windows 10が使いにくいと言われる15の理由とその対策
Windows 10は世界的に有名なオペレーティングシステムであり、定期的なアップデートによって信頼性を維持している点が特徴です。搭載されているアプリやウィジェットは完璧ではありませんが、十分に実用的です。しかし、設定や機能にはまだ改善の余地があります。 Microsoftは全世界で約13億人のWindows 10ユーザーを抱えていますが、「Windows 10は使えない」と感じている人も少なくありません。その理由は、日常的に発生するさまざまな問題にあります。例えば、エクスプローラーの不具合、VMwareとの互換性問題、データ消失などです。また、Windows 10 Proはファイル階層の管