Money Forward Developers Blog

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

20230215130734

最近のruby-core (2016年11月)

こんにちは。卜部です。

ruby-coreというRuby本体の開発の議論がされているメーリングリストがあります。

というか、議論がどうとか以前の話ですが、Rubyにおいては12月がリリースの季節です。脆弱性の修正などはさておき、新機能の入った新バージョンはもうずっと毎年12月に出ています。その直前は、プロダクトクオリティの上昇を目的として新機能の追加は凍結されます。たぶんこの記事が出た頃にはもう凍結されてることでしょう。

というわけですので毎年11月には駆け込みで新機能が追加されそうになるものです。今年は11月にカンファレンスが連続していたという事情もあり、ようは駆け込みが沢山あります。ええ……

全部は解説できませんが、興味深かったものをいくつか。

[#12886] URI#merge doesn't handle paths correctly

URI.parse("/foo/bar") + URI.parse("baz")

みたいなやつが動かない、という報告で、動かないのはその通りかと思います。

しかしながらURIにおいてパスって何ですかというのはそこまで明らかな話ではない。たとえばmailtoとかdataとか、なんかあんまりパスっぽくないURIもあるわけです。なので一般的なURIにおいてパスぽいのをなんとなく動かしてくれというのは無理筋だと思います。

まあ要はURIという発想自体があんまり理解されてないということかもしれませんね。結局みなさんが普段使うのはhttpsとかしかないし。

[#12553] IO.readlines(filename, chomp: true)

IOから文字列を読み込む時に、えてして行単位で読み込みたくなり、しかし改行は不要ということが多々ありますね。たとえばjsonlというファイルフォーマットなどはそういう、改行で区切られてるけど改行文字自体は不要なやつです。

で、これまではそういうのは一旦改行で区切ってからchompで改行を取り除くなどしていたわけですが、操作的な書き方になってしまうし、そもそも頻出なので本体側で処理してあげたほうがいい。

というわけでIO.readlinesの引数が増えて、改行文字をあらかじめ取り除いておいてくれるようになりました。

[#12607] Ruby needs an atomic integer

Rubyではx += 1みたいな操作がアトミックではないので、スレッド間で割り込まれることがありえます。それはいいんだ別に。Rubyに限った話じゃないしね。他の言語でもそうですよ。ただ問題はどう回避するかという話で。今のところRubyだといきなりMutexで保護するしかない。

それで、カウンターを作りたいだけなのにいきなりMutexとか登場するのはさすがに牛刀割鶏という話じゃないかと思うわけですよ。やりたいことはただのLOCK CMPXCHGじゃないですか。やりたいことに対して実現するための下準備が大きすぎる。という、自分の意見だったのですが。

これに笹田さんは反対で、Mutexはともかくわかりやすいからいいんだと。マルチスレッドプログラミングは難しいから、たとえばアトミック整数みたいなものを導入して、よくわかってないユーザーがマルチスレッドできてしまうかのような幻想を与えるのは良くない。わかってない人はMutexを使うことで、ロックの箇所がより明確になるという主張をしています。

立場の違いなので、このまま平行線かもしれないです。

[#5446] at_fork callback API

これなー。pthread_atfork(3)みたいなforkのフックが欲しいという主張で、まあ子プロセス側が親プロセスと資源を共有しないようにいろいろと後始末したいという思いはわからなくもない。キューとか。

しかしながらこれは無理なんですよ。子プロセスがforkした後で子プロセス側で動かすことができるプログラムというのは(Rubyは一般的に言ってマルチスレッドプログラムなので)大変に制限されていて、async-signal-safeな関数しか呼べないので、ようはRubyのプログラムを動かすということは、できないんです。

上記のpthread_atforkのリンク先にも長い長いRATIONALEが記載されている通り、pthread_atforkの設計時の意図としては、forkする前にMutexとかを適切にロックしておいてから、fork後に親側と子側でそれぞれにロックを解除するという趣向だったわけですが、長い議論の結果結局それはうまくいかないということになったわけです。Cでもです。なので結局用途があんまりないので、pthread_atforkは将来的にdeprecatedにするぞ!と(少なくともPOSIX issue 7の時点では)書いてあるわけ。

だからRubyが悪いというよりはこれは、スレッドとforkが混ざった時の辛さの話なので、人類には難しすぎましたねという感想にしかならなそうです。

[#12142] Hash tables with open addressing

Untitled.png (17.0 kB)

前からこの連載で追いかけてたので既によくごぞんじかもしれませんが、Hashの内部構造が大きく変わりました。

どちらかというと高速化に寄与しているのは、これまでエントリ一個一個をmallocでちまちまと確保していたところを一個の大きなホモジニアスなエントリの配列にパッキングするようになったことです(と思ってます)。これによりハッシュを走査するメソッド(Hash#eachとかの大量に使う系のやつ)がただの配列をなめる走査になったのでポインタをたぐったりせずに高速になっています。

その隣に検索をO(1)で実現するための、DBでいえばインデックスに相当するやつがくっついていて、こちらがopen addressingで作られています。前にも解説しましたが、ハッシュ値(のmod)が衝突したときにはハッシュ値を種とする線形合同法による擬似乱数にて次の候補を得るという方式は面白いです。これにより解空間の充填性とamortized O(1)アクセスを理論的に保障しています。

なお同じコレクションにどんどん値を出し入れしていくと、消したやつの部分は空き領域ってことでどんどんゴミがたまっていくようになっています。そのため、エントリの配列がいっぱいになった時はコンパクションが走るようになっていて、メモリリークが防がれています。

[#12902] How about Enumerable#sum uses initial value rather than 0 as default?

今回Enumerableにsumというメソッドが追加になっているのですが、空配列とかに向けて使うと0が返ってくるようになっています。つまり[0].sum[].sumは同じ型になっているわけですね。しかしここでたとえば文字列の配列などに対してsumを考えることはできるはずなのに、いまだと文字列の配列にsumすると0と足せなくて例外になってしまう。なので、デフォルトの初期値(「加法単位元」って言えばいいのかな)を0じゃないようにしよう、という提案です。

とはいうものの、たとえば['foo'].sum[].sumの型が整合しないのは回避できないし、文字列ならそんなことせずにjoin使えよという正論はあるわけですね。

あと、おそらく引数やselfの中身で返ってくる型が違うことに対する警戒感は以前IO#readで引数が0の時の返り値を変えた時の話などが念頭にあるのだと思います。

この問題を綺麗に解決するには配列が何でも入れられる配列というだけでは加法単位元が決定できなくてうまくいかないのだから、もっと総称型配列のようなものを導入する必要があると思います。

[#12484] Optimizing Rational

2015年のRuby Association開発助成(っていう正式名称でいいんでしょうか)の開発成果のうちのひとつです。

従来、そもそもRationalの歴史をひもとくとごく初期はRubyで書かれていたものが、その後Cに翻訳されて、本体のコア機能として取り込まれ、今では専用リテラルが導入されるまでになり、ようは用途が広がってきています。ところが初期がRubyで書かれていたという事情から、実装面で最適化の余地を残していました。今回、内部の計算がメソッド呼び出しを経由しないように書き直されて、高速化が図られています。

これといって仕様が変わったわけではないですが、Timeなど内部でRationalを使っているものも今ではあるので、全般的なインパクトは大きいです。

[#12515] Create "Boolean" superclass of TrueClass / FalseClass

だいぶ前にすでにrejectされているこのissueがまだ盛り上がっていて、rejectされてからの方がスレッドが長くなっているため、Booleanクラスの旺盛な需要を再確認させられます。

いろいろなBooleanを必要とする理由やその反論などが語られていますけれども、結局これはオブジェクト指向とは何か、型とは、宇宙、人生、瞑想、野菜とかのレベルの議論の気がするんですよね。Rubyのクラスというのは、たとえばstructの別名ではないわけですけれども、そのへんに対する理解だったり、あるいは他のJSONみたいなやつだったりとのインターフェースだったりする部分にあまりこれまで光が当たってこなかったのかなと思います。

[#4147] Array#sample で重みを指定したい

古いスレッドが浮上してきていました。もともとはおみくじのような、一様ではない確率分布に従うやつが欲しい。という話から始まったのですが、そこで「おみくじのような」と言ってしまったので、復元抽出なのかどうか、また実行効率について計算量の悪化など、かなり要求仕様に関する議論が大きくなってしまって、話が大きすぎてまとまらない状態になってしまったものです。この頃にはすでに「話を大きくすると入れづらくなる」というのは経験則として広まっていたし、スレッドの中でも言及されていますが、それでも難しかった。

現状だとちょっと、一体何をすればいいかよくわからない感じになってしまっているので、もっと小さな仕様変更を積み重ねる方向で分解して出し直すのが良いと思います。

[#12954] valgrind shows memory leaks

valgrind (というメモリチェッカー、便利なので知らなかった人は是非使うべき)がメモリリークをたくさん報告してくるんですがという報告です。

しかしながら、たとえばクラス定義とかは、実際問題スクリプトが生きてる間はずっと消せないわけですので、ようはプロセスが終了するまで放置するより他ないわけです。実際にvalgrindが報告するメモリリークはこれらを検出しているものです。

こういう種類のメモリ領域はべつにRubyに限らず発生するわけですが、プロセスが終了する時に律儀にfreeして回るべきか、という論点にはfj.comp.lang.c++以来のひじょうに長い議論の歴史があり、少なくともすべき派/すべきでない派それぞれの主張に一理ずつはあるという状況かと思います。

実際、よそを見ると、C++11以降ではstd::quick_exit()という関数が追加になり、プロセス終了時に生きてるオブジェクトに対してデストラクタを走らせないモードが選べるようになりました。freeしない派の意見も尊重されつつあるようです。

もしfreeすべき派の人がこのスレッドに参戦するのであれば、その理由が「valgrindがうるさいから」だけではちょっと弱いんじゃないかと思いました。

[#12770] Hash#left_merge

新しいメソッドの提案で、名前から推測すると、おそらくSQLにあるLEFT OUTER JOINが念頭にあるのだと思います。現在のHashに定義されているHash#mergeがleft join的ではないのは事実だし、SQLのleft joinに需要があること自体は否定できないものがあります。

なんだけど、どうも読んでいるとleftぽくないというか?なんだろう、ちょっと不思議な挙動を目指してるように読めてしまうんですね。とくにnilのまわりの振る舞いがあまりうまくなさそうで、SQLでいえばNULL入れていい列を含むようなleft joinを投げたらNULLもきちんと左が残るはずなんですが(ですよね?)。でもどうも提案はそうはなってなさそう。

というわけで、この提案自体は残念な結果になってしまいましたが、それは提案の細部がふんわりしているのが理由なので、leftという発想そのものが否定されているわけではないはずだと思います。もうちょっと練り直して再挑戦して欲しいですね。

[#11904] Why was Thread.exclusive deprecated?

これも浮上してきた古いスレッドで。主張としてはThread.exclusiveは必要なのでなくさないでほしいという話です。なくす側のモチベーションとしては、スレッド間の排他制御は明示的にMutexを利用してやるべきで、グローバルなThread.exclusiveみたいな記述をすべきではないという、哲学というか、そういう行儀の良さを主張していきたい。

しかしよく議論を追いかけていくと、たしかに今、Thread.exclusiveをなくしてしまうと困るシチュエーションが存在しそうです。本当にMutexで明示的にロックをかけるのであればMutexをどこかのタイミングで初期化する必要があるのは当然ですが、Mutexを初期化する部分を保護するMutexが必要という、鶏卵状態がある。ライブラリ作者はライブラリの中で自分が使うMutexを初期化すべきですが、ライブラリをrequireする部分がマルチスレッドで複数スレッドから一斉に呼ばれた時に、複数のスレッドが同時にMutex初期化してしまわないか、どうも怪しい感じになりそうで、そこを保護するためにもグローバルなロックがある方が良い、という主張がされています。

その主張には一理あるとは思うのですが、一方でMutexの初期化ごときのためにグローバルに待ち合わせが走るというのはいかにも話が大きすぎるんじゃないでしょうか。ようは必要なのはonceですよね…

[#12980] Time - Time to return a Rational

上にもちょっと書いたけれど、Timeというのは内部表現にRationalが使われている(ことがある)のに、その差分がRationalになってないのはなぜか、という疑問です。

なお念のために用語を整理するとTimeというのは「時刻」を表すクラスであって「時間差」の話ではないので、時刻同士の差分がTimeクラスになりえないのは自然な話かと思いますが、じゃあ何になるべきかというのがこの問題。

今、ようはTimeどうしの差分でRationalが返ってくるのは3.9+5.1が「9」になるみたいな気持ち悪さがあるという理由でFloatになっているわけですけれども。つまりTime.nowとかやって返ってくる時刻はミリ秒だかナノ秒だかしらんけどもなんかの解像度で取得されてくるわけで、これは3.9みたいなやつなわけですよ。それがTimeオブジェクトの中でRationalを使って保存されているのは、たまたま、doubleで保存すると解像度が不足して下の方の桁が保存しきれない場合があることに備えているにすぎない(という理解です)。ようは実装詳細なので、外の方までRationalの振る舞いが見えてくるのは違うんじゃないか、Timeの背後にあるinexactな性質は、きちんと表現したほうがいいのではないかという理由で、これは意図的にFloatが返ってきているのでした。

ただ、個人的には本当にきちんとTimeのバックエンドの精度を管理する気があるならFloatは不適切で、BigDecimalをちゃんと精度指定して使うべきだと思っているので、現状は(もしきちんとを守る気でやってるなら)なんか中途半端なんじゃないかな、とは思ってます。精度って1bitの情報じゃないはずなので。さはさりながらBigDecimalはやりすぎじゃないのというのも分かるんですけどね。

[#12979] Avoid exception for #dup on Integer (and similar cases)

ここにきて突如Integer#dupが可能になるというコペルニクス的転換が…

そもそもdupというのはオブジェクトのコピペをするときに使うやつなわけですけれども、整数とかいくつかのオブジェクトは最初からimmutableにできているので、コピペがそもそも必要なくて、好きなだけシェアして使えばいい(副作用が何もないから)という理由で、これまではdupなどは例外になるように作られていました。

しかしながら、アルゴリズム全体がimmutableに作られている場合が最近は増えてきているのですが、そういう場合だとオブジェクトを壊さないようにdupして回るという場面は結構あります。そういうときに、dupできるオブジェクトとできないオブジェクトが入り混じっていると「これはdupできるから〜」みたいな不毛な場合わけが必要になってきて、それはどうなのという話なわけです。本来は「副作用がないから気にするな」の意図だったのに、現状は「例外が起きちゃうから気にしないと」なわけです。これは不毛です。

というわけでInteger#dupは、呼ばれると、あたかも正常にコピペされたかのように、しかし実体としてはコピペではなく、それ自身を黙って返すようになりました。

[#12786] String#casecmp?

そもそもPOSIXにstrcasecmp(3)という関数が定義されていて、文字列の大文字小文字を区別しない「辞書順比較」を行うものです。これに対応するString#casecmp(注: ?なし)というメソッドがすでにrubyにもあります。

しかしながらここで、Unicodeという大きな問題があるわけです。文字列の辞書順を決定するには当然、「こちらのほうが後ろ」とか言えないといけないわけですが、一方でUnicodeで大文字小文字を無視した辞書順というのは大変難しい。大文字と小文字の文字数が違う(たとえばU+00DF)とか、小文字コードポイントのほうが大文字よりずんと前に乖離している(たとえばU+212A)とか、いろいろな場合があるので、順序をきちんと実装するにはおそらくUnicodeの全空間の直積で大小関係を定義していく必要があるはずと思います。

このような事情によりString#casecmpはUnicodeに対応していません(ASCIIの範囲内でしか動かない)。

ところが、ここで問題を「辞書順」ではなくて「等価性判定」に限定して考えると、実は問題の複雑さがかなり緩和される。大文字小文字を変換するのは、複雑とはいえ今回のバージョンのrubyから可能になりました。そこで大文字か小文字かどちらかに寄せてしまえば、その等価性を言うことは可能になってくる。どっちが先でどっちが後かはわからないながらも、違うものは違うとまでは言えるわけですよ。

これは問題の切り分けとしてはかなり賢くて、だって、普通あんまり大文字小文字無視した辞書順とかやらないじゃないですか。辞書順ソートが必要な時はだいたい大文字小文字無視しない順ですよね。大文字小文字無視したいときはだいたいが比較したいだけという場合が多くて、そういう場合向けのメソッドを用意するというのは実用上で有益と思われます。

というわけでtrue/falseを返すString#casecmp?が新設されました。

最後に

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

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

【プロダクト一覧】 ■家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 iPhone,iPad家計簿アプリ・クラウド家計簿ソフト『マネーフォワード』 Androidクラウド型会計ソフト『MFクラウド会計』クラウド型請求書管理ソフト『MFクラウド請求書』クラウド型給与計算ソフト『MFクラウド給与』経費精算システム『MFクラウド経費』消込ソフト・システム『MFクラウド消込』マイナンバー対応『MFクラウドマイナンバー』創業支援トータルサービス『MFクラウド創業支援サービス』お金に関する正しい知識やお得な情報を発信するウェブメディア『マネトク!』