Money Forward Developers Blog

株式会社マネーフォワード公式開発者向けブログです。技術や開発手法、イベント登壇などを発信します。サービスに関するご質問は、各サービス窓口までご連絡ください。

20230215130734

最近のruby-core (2017年1月)

こんにちは。卜部です。最近のPython-devが始まりましたね。すごい。

こちらの連載は先月はお休みしてしまったのですが、引き続き頑張ります。 ruby-coreというRuby本体の開発の議論がされているメーリングリストで、最近興味深かったトピックを紹介していきます。


[#12852] URI.parse can't handle non-ascii URIs

Railsがよく ?utf8=✓ とかいうクエリをつけてきますよね。で、それをRuby標準のURIライブラリでは取り扱えないのですけれども。取り扱えないのは事実で。

ただURIというのはそういうものでして。URIの定義からいえば✓という文字はそもそも使えないので、そういう入力は定義からいって不正。なのでエラーになるというのは筋そのものは通ってる。

ようは「こっちは正しく作ってんだよ」vs「正しくたって使い物になってねえんだよ」の戦いなんで、どっちにも一理あるので、こういうのは膠着状態になりがちですね。

実状に合わせて仕様の方が変わることを望む、という締めになっていて、まあ、そうだよねという感想です。

[#9569] SecureRandom should try /dev/urandom first

以前Hacker Newsなどで炎上していた/dev/urandomの件ですが、一応の解決を見たのでご紹介。これまでの炎上の流れをごく簡単にまとめると

  • 提案: OpenSSLなどに依存することなく、Kernelが提供しているCSPRNGである/dev/urandomを使うべきである。なぜならOpenSSLに依存する必要がなく、依存しないほうがセキュリティが強固になるから。
  • 反論: Linuxのmanに「/dev/urandomをそういう用途で使うな」と書いてあるからNG。
  • 反論: manの作者は間違っている。実装は問題ない。
  • 反論: じゃあまずmanを直そうね。
  • 反論: manじゃなくてセキュリティの話をしてるんだよ。
  • 反論: セキュリティの話ってんならOpenSSL危ないって事でしょ、ならRubyを直す前にやることがあるんじゃないの。

というかんじ。雰囲気悪いですね。

で、このチケット自体はしばらく膠着状態だったのですが、この間に志あるセキュリティ専門家たちが正面からLinuxのman-pagesの改修に取り組んでくれていたようで、先月にリリースされたバージョン4.09でついに/dev/urandomに関する記述が妥当なものに変わっていました。のみならず、Linuxで何種類かある乱数源のどれを選べば良いかのガイドラインとして新たにrandom(7)が書き下ろされて、かなりわかりやすくなっています。

そういうわけで「まずmanを直そう」が肯定的に解決されたので、反対する理由が消滅して、この変更は入りました。

[#4840] Allow returning from require

急に来た…

メソッドの中じゃないところからreturnできるようになりました。つまり

return unless $0 == __FILE__

が書けるようになった。returnが実行されたらそのファイルのことはそこでおしまいです。これは、ライブラリとしても実行ファイルとしても使えるような小さいプロジェクトではしばしば便利です。

でも12月21日に入れなくてもいいじゃん。

[#7360] Adding Pathname#glob

Pathnameというのはファイルパスを文字列じゃなくてオブジェクトとして扱おうというライブラリですが、いまPathnameを使ってglobする機能がありません。

機能自体は単に作り込んでなかっただけの話なので、作ればいいんですが、しかし考えていくと、これは既存の機能を組み合わせただけでは実現できない。たとえば ~/*という邪悪な名前のディレクトリを作ることができますね。実際問題。なのでそれに対応するPathnameオブジェクトは作成できてしかるべきです。そこで、そのディレクトリを起点にしてglobするとどうなるだろうか? というと、やはり、その隣のディレクトリなんかがヒットしてくるのは違うわけです。*というのは、ここでは実在のディレクトリ名なのであって、globのメタ文字じゃない。

というわけで、これを実現するには「特定のディレクトリを起点にするglob」という機能がそもそも必要なことがわかります。openat(2)みたいな感じですね。その機能を作るチケットとして[#13056]が作成され、こちらは一旦ブロッカー待ちという状況になりました。

[#12733] Bundle bundler to ruby core

これはまだ確定ではなく、かつ動きもない状態ですが、パッケージマネージャrubygemsとbundler(どちらもRuby本体とは別のプロジェクト)が合流しそうな気配があります。

基本的には別プロジェクトの動きなので、本体側から積極的にどうこう言う話ではないのですが、まあとはいえ影響は大きいので、「注視している」という感じですね。

実際に合流したあかつきにはrubyをインストールするとbundleも付いてくる状態になるのだと思います。

[#5481] Gemifying Ruby standard library

この連載を読んでいる人なら少しずついろいろなライブラリがruby本体から分離していっているのに気づいているかもしれません。

歴史的に、まだrubygemsができる前から本体に存在していたライブラリが色々とあるわけですけれども。当時は本体と一緒に配るしかなかったとはいえ、今はそんなことしなくてもgemにするほうが素直だし、身軽(たとえば、一行のパッチを配布するためにruby全体をリリースするのはためらわれる)になるし、という状況があります。

そこで今年はどういう方向で進行しようかという議論が行われているのがこのチケット(長い)です。もちろん、まだまだやることはあるし、リリースまでも長いので、今の時点でなにか決まったことはないです。

とりあえずの動きとしては今本体に含まれているいろいろなライブラリにまずgemspecを書いていこうという流れのようですね。ただ、もう一気にやっちゃおうという発言には「メンテナがいるものは説得してから」という返答がかえっており、まあ妥当な意見ではあるので、それも含めて一歩ずつというところでしょうか。

[#12926] -l flag for line end processing should use chomp! instead of chop!

-lというのは普通は-n-pと混ぜてperl -anle '…'という感じで、いや、ruby -anle '…'か。まあそう使うやつです。

ともかくワンライナー用のオプションであるところからもおわかりのようにperl由来なわけですけれども、いま、なぜか、perlと振る舞いが違うんですね。perlの-lはchompだけどrubyはchopなわけです。なぜか?

これ理由面白くて、そもそもrubyに-lを実装した時、まだrubyにもperlにもchompがそもそも実装されてなかったんですって。なので当時としては振る舞いが一緒だった。その後perlにchompが来たからrubyにもchompを付けたけど、その時-lが変わってることには気づいてなかったんだそうですよ。

というわけで現在確認できる最古のrubyである1994年7月19日のバージョン0.49を見ると、この時点でたしかにすでに-lはある。コンパイルできないから確認できないけど。コードを読む感じではすでにchopですね(proc_options()yywhole_loop()が当該部分だ)。で、94年というのはまだperlが4だった時代で、たしかにperlにchompが入ったより前だ。すごい。で、ruby側にchompが入ったのは…98年ですかね? ここで-lを変更し忘れたのか。

というわけでこれは19年物のバグというすごい調査結果になったので、修正されました。すごいなー。残ってるんだなー。こういうの。

[#12912] An endless range (1..)

たとえばEnumerableというモジュールはeachメソッドの上に構築されている、というのは割と有名かと思いますが、実はRangeもsuccメソッドの上に構築されているものです(注: ここでいう「上に構築」とは「あれば作れる」くらいの意味。eachメソッドが定義してあるクラスはEnumerableになれるし、succメソッドが定義してあるクラスはRangeになれる…なれるの意味がちょっと違うけど…ということ)。

ところで、今だとRangeオブジェクトは、範囲なんだから当然、始まりがあって終わりがあるわけですけれども、succメソッドそのものは別段、終わりがあることを要請しているわけではありません。またArray#[]などRubyの一部の機能では 1..-1のような(直感に反する)Rangeを受け付けることがありますが。この-1というのは、なんか知らんけど必要なだけ無限に最後まで的な意味のマーカーになっているという事実があります。つまり終わりは、ないことがある。けど今だと終わりがないとRangeにできないから、とりあえずくっつけてあるけど、本当は不要。

さらにいうと、(チケットに書いてありませんが)多次元配列から列ベクトルを抜き出したい時に、たとえばNumPyだと

a = array([[1,2], [3,4], [5,6]])
a[:, 1] # array([2, 4, 6])

とか書けるわけですが。この:は何か。これはPythonではsliceと呼ばれているやつですけれども、意味としては「はじまりも終わりもないRange」っぽく解釈してもさほど、違和感がない。

したがってRubyにおいてRangeの終わりが、なくても良いのではないかというのは、実はさほど突拍子もない意見ではありません。

とはいうものの新しい文法を入れるのであれば、やはりPythonでは書けるんだから1:2:3みたいにstepを書きたくなるのが人情で、そうするとこの提案のように(1..)という拡張にしてしまうと、拡張する余地がないということになってしまって、これはよくなさそうです。

意味はあってもいいと思うんだけど、文法をどうするかが悩みどころですね。

[#12906] do/end blocks work with ensure/rescue/else

2.5からの新文法、do...endにrescueがつけられるようになりました、つまり

obj.method(arg) do |var|
  raise something
rescue
  log $!.message
end

これが書けるようになった。

これまでも同様の提案がいくつかあったのですが、今回の提案が通ったのは、過去の同案と違いあくまで do...end の拡張だけにフォーカスしており影響範囲も明確、パッチも1行できれいに当たるという「行儀の良さ」にあるとおもいます。

新機能、とくに新文法の追加は気宇壮大になりがちですが、大きな変更になればなるほど入れるのが大変なわけでして、あくまで着地が目標なのであれば、こういうふうに小さくまとめるのが重要なんだなと改めて思いました。

[#13017] Switch SipHash from SipHash24 to SipHash13

Hashクラスで使われているハッシュ関数を変えようという提案です。

これですが、ハッシュ関数は一様に分布すればいいよという時代はかなり昔に終了していまして、いまどきはハッシュ関数を意図的に衝突させてDoSに使うというhashDoS攻撃が知られているので、一様な分布だけでなく、DoS耐性も求められています。とはいえ、もちろんたとえばSHA3とかのHMACだと衝突耐性はものすごく高いだろうと思われる反面、そういうのはそんなに速くないので、実用上はHashのハッシュ関数として使うのは難しい。DoS耐性と高速性はある程度相反する要件なので、バランスが難しいわけです。

これまで使われていたハッシュ関数のSipHash24というのも、過去に脆弱性の報告があったときに強さを基準に選ばれたものですが、今回の提案は、これを少し強度を下げてSipHash13というややショートカットした実装にしても大丈夫じゃない? という提案です。利点はもちろん高速化です。

ただ、最初の時点で「なんでそれで平気なの」が書かれておらず、ちょっと判断つかなくて迷走してしまったのはよくなかったですね。「作者が大丈夫って言ってるから」じゃあちょっとねえ。

結局、第三者による解析結果の論文が出てきて、「今のところ、SipHash13を解析して意図的に衝突を作る場合、最良の確率(2-167)で衝突させても誕生日攻撃(2-128)よりも確率が低い」という結果のようなので、誕生日攻撃は防ぎようがないし、まあそれより効率的な攻撃がないのなら良いのでは、ということになって、この提案は採用になりました。

もちろんこれは将来方向に向けて絶対大丈夫という意味ではありませんが、ハッシュ関数の攻撃耐性というのはそういうもので、なんか見つかったら対策するしかないのは何を使ってても同じなので、それはしょうがないかな、と。

[#12962] Feature Proposal: Extend 'protected' to support module friendship

ある程度大きなクラスになってくると、内部で使うだけのユーティリティ的な用途のメソッドと、外部に公開する用途のものができてくる。これは良いと思うのですが、さらに大きくなってくると複数のクラスから使われるユーティリティ的なメソッドというものも発生してくることがあります。しかし、それはあくまでユーティリティなので、あまり外部に公開したいものではない。なのに今だとそういうものも全部publicにするしかない。この用途をどうにかして明確に表現したい。このリクエストが解決したい問題はそういう状況で、したがってfriendという表現になっているのだと思います。

とはいうものの、protectedを使おうという発想はよくなかった。既存のprotectedの挙動を変えるのは影響が大きすぎるし、そもそもprotectedは失敗だったと繰り返し語られているので、今更便利に使おうと主張しても通りづらいですね。それよりはぜんぜん新しい可視性を増やそうという [#9992]のほうが、まだめがありそうに思いました。

[#13152] Numeric parsing differences between ruby <-> crystal

単項のマイナスと累乗の演算子がどっちが強いかという話です。

- 2 ** 4 # - (2 ** 4) or (-2) ** 4 ?

もちろん報告者はRubyとCrystalで振る舞いが違うと報告してきていて、まあそこで「他の言語なんか知らん」と返しても良かったのですが、ふと調べてみるとこれは、ある程度どちらの側の例もあって、片方が間違いとも言えないようなのですね。興味深かったのは

  • HaskellはRubyと同じだがOCamlはCrystalと同じで、両者が異なる(意外だ)
  • JSは明示的に括弧をつけないと通らない(賢い!)

あたりでしょうか。

[#12901] Anonymous functions without scope lookup overhead

Rubyの場合lambdaのほうが普通のメソッドより有意に遅いという問題が指摘されています。これは、何が違うかというと、もちろんlambdaには環境が閉包されていることが原因で、逆に言うとRubyのメソッドは環境を持っていない分、スコープの検索などが軽いということができます。

そこで、提案は環境を閉包しないようなlambdaの亜種を作ろうという提案なのですが、そんなことするより不要な環境とか勝手に不要って判定して賢く最適化してくれた方がいいじゃん? 実際Lisp系言語だったらclosureに中で使ってない不要な変数束縛を残しとくような無駄な実装には普通はならんわけです。そういうのプログラマ側に押し付けるんじゃなくて、勝手に処理系でやったほうがいいに決まってますよね。

ようするに根本的な問題として、なぜlambdaが最適化できないのか? というのが背景にあって、今の言語仕様だとlambdaを作った後でそのbindingを外部から抽出したり注入したりできちゃう。だからlambdaのスコープから見える変数は全部残しておかないと、lambdaのプログラム部分で使ってなくてもbindingからそこをたどって取得できちゃう。だから削れない。

ここをどうにかする必要があるというのが、ささださんの意見で、どうにかする提案が、この後来ることになってるようです。要注目ですね。

最後に

マネーフォワードでは、ruby-coreが気になって夜も眠れないエンジニアを募集しています。 ご応募お待ちしています。

【採用サイト】 ■マネーフォワード採用サイトWantedly | マネーフォワード

【プロダクト一覧】 自動家計簿・資産管理サービス『マネーフォワード』 ■WebiPhone,iPadAndroid

ビジネス向けクラウドサービス『MFクラウドシリーズ』 ■会計ソフト『MFクラウド会計』確定申告ソフト『MFクラウド確定申告』請求書管理ソフト『MFクラウド請求書』給与計算ソフト『MFクラウド給与』経費精算ソフト『MFクラウド経費』入金消込ソフト『MFクラウド消込』マイナンバー管理ソフト『MFクラウドマイナンバー』資金調達サービス『MFクラウドファイナンス』

メディア ■くらしの経済メディア『MONEY PLUS』