engineers_blogこんにちは。ruby-coreというRuby本体の開発の議論がされているメーリングリストがあります。いうか、議論がどうとか以前の話ですが、Rubyにおいては12月がリリースの季節です。脆弱性の修正などはさておき、新機能の入った新バージョンはもうずっと毎年12月に出ています。その直前は、プロダクトクオリティの上昇を目的として新機能の追加は凍結されます。この記事が出た頃にはもう凍結されてることでしょう。いうわけですので毎年11月には駆け込みで新機能が追加されそうになるものです。今年は11月にカンファレンスが連続していたという事情もあり、ようは駆け込みが沢山あります。ええ......全部は解説できませんが、興味深かったものをいくつか。URI.parse("baz")みたいなやつが動かない、という報告で、動かないのはその通りかと思います。URIにおいてパスって何ですかというのはそこまで明らかな話ではない。mailtoとかdataとか、なんかあんまりパスっぽくないURIもあるわけです。一般的なURIにおいてパスぽいのをなんとなく動かしてくれというのは無理筋だと思います。要はURIという発想自体があんまり理解されてないということかもしれませんね。結局みなさんが普段使うのはhttpsとかしかないし。true)IOから文字列を読み込む時に、えてして行単位で読み込みたくなり、しかし改行は不要ということが多々ありますね。jsonlというファイルフォーマットなどはそういう、改行で区切られてるけど改行文字自体は不要なやつです。これまではそういうのは一旦改行で区切ってからchompで改行を取り除くなどしていたわけですが、操作的な書き方になってしまうし、そもそも頻出なので本体側で処理してあげたほうがいい。いうわけでIO.readlinesの引数が増えて、改行文字をあらかじめ取り除いておいてくれるようになりました。1みたいな操作がアトミックではないので、スレッド間で割り込まれることがありえます。Rubyに限った話じゃないしね。他の言語でもそうですよ。問題はどう回避するかという話で。今のところRubyだといきなりMutexで保護するしかない。それで、カウンターを作りたいだけなのにいきなりMutexとか登場するのはさすがに牛刀割鶏という話じゃないかと思うわけですよ。CMPXCHGじゃないですか。やりたいことに対して実現するための下準備が大きすぎる。いう、自分の意見だったのですが。これに笹田さんは反対で、Mutexはともかくわかりやすいからいいんだと。マルチスレッドプログラミングは難しいから、たとえばアトミック整数みたいなものを導入して、よくわかってないユーザーがマルチスレッドできてしまうかのような幻想を与えるのは良くない。わかってない人はMutexを使うことで、ロックの箇所がより明確になるという主張をしています。立場の違いなので、このまま平行線かもしれないです。pthread_atfork(3)みたいなforkのフックが欲しいという主張で、まあ子プロセス側が親プロセスと資源を共有しないようにいろいろと後始末したいという思いはわからなくもない。これは無理なんですよ。子プロセスがforkした後で子プロセス側で動かすことができるプログラムというのは(Rubyは一般的に言ってマルチスレッドプログラムなので)大変に制限されていて、async-signal-safeな関数しか呼べないので、ようはRubyのプログラムを動かすということは、できないんです。上記のpthread_atforkのリンク先にも長い長いRATIONALEが記載されている通り、pthread_atforkの設計時の意図としては、forkする前にMutexとかを適切にロックしておいてから、fork後に親側と子側でそれぞれにロックを解除するという趣向だったわけですが、長い議論の結果結局それはうまくいかないということになったわけです。7の時点では)書いてあるわけ。Rubyが悪いというよりはこれは、スレッドとforkが混ざった時の辛さの話なので、人類には難しすぎましたねという感想にしかならなそうです。addressing前からこの連載で追いかけてたので既によくごぞんじかもしれませんが、Hashの内部構造が大きく変わりました。どちらかというと高速化に寄与しているのは、これまでエントリ一個一個をmallocでちまちまと確保していたところを一個の大きなホモジニアスなエントリの配列にパッキングするようになったことです(と思ってます)。これによりハッシュを走査するメソッド(Hash#eachとかの大量に使う系のやつ)がただの配列をなめる走査になったのでポインタをたぐったりせずに高速になっています。addressingで作られています。前にも解説しましたが、ハッシュ値(のmod)が衝突したときにはハッシュ値を種とする線形合同法による擬似乱数にて次の候補を得るという方式は面白いです。O(1)アクセスを理論的に保障しています。同じコレクションにどんどん値を出し入れしていくと、消したやつの部分は空き領域ってことでどんどんゴミがたまっていくようになっています。そのため、エントリの配列がいっぱいになった時はコンパクションが走るようになっていて、メモリリークが防がれています。default?今回Enumerableにsumというメソッドが追加になっているのですが、空配列とかに向けて使うと0が返ってくるようになっています。[0].sumと[].sumは同じ型になっているわけですね。ここでたとえば文字列の配列などに対してsumを考えることはできるはずなのに、いまだと文字列の配列にsumすると0と足せなくて例外になってしまう。デフォルトの初期値(「加法単位元」って言えばいいのかな)を0じゃないようにしよう、という提案です。いうものの、たとえば['foo'].sumと[].sumの型が整合しないのは回避できないし、文字列ならそんなことせずにjoin使えよという正論はあるわけですね。あと、おそらく引数やselfの中身で返ってくる型が違うことに対する警戒感は以前IO#readで引数が0の時の返り値を変えた時の話などが念頭にあるのだと思います。この問題を綺麗に解決するには配列が何でも入れられる配列というだけでは加法単位元が決定できなくてうまくいかないのだから、もっと総称型配列のようなものを導入する必要があると思います。Association開発助成(っていう正式名称でいいんでしょうか)の開発成果のうちのひとつです。従来、そもそもRationalの歴史をひもとくとごく初期はRubyで書かれていたものが、その後Cに翻訳されて、本体のコア機能として取り込まれ、今では専用リテラルが導入されるまでになり、ようは用途が広がってきています。ところが初期がRubyで書かれていたという事情から、実装面で最適化の余地を残していました。今回、内部の計算がメソッド呼び出しを経由しないように書き直されて、高速化が図られています。これといって仕様が変わったわけではないですが、Timeなど内部でRationalを使っているものも今ではあるので、全般的なインパクトは大きいです。FalseClassだいぶ前にすでにrejectされているこのissueがまだ盛り上がっていて、rejectされてからの方がスレッドが長くなっているため、Booleanクラスの旺盛な需要を再確認させられます。レベルの議論の気がするんですよね。Rubyのクラスというのは、たとえばstructの別名ではないわけですけれども、そのへんに対する理解だったり、あるいは他のJSONみたいなやつだったりとのインターフェースだったりする部分にあまりこれまで光が当たってこなかったのかなと思います。重みを指定したい古いスレッドが浮上してきていました。おみくじのような、一様ではない確率分布に従うやつが欲しい。いう話から始まったのですが、そこで「おみくじのような」と言ってしまったので、復元抽出なのかどうか、また実行効率について計算量の悪化など、かなり要求仕様に関する議論が大きくなってしまって、話が大きすぎてまとまらない状態になってしまったものです。この頃にはすでに「話を大きくすると入れづらくなる」というのは経験則として広まっていたし、スレッドの中でも言及されていますが、それでも難しかった。現状だとちょっと、一体何をすればいいかよくわからない感じになってしまっているので、もっと小さな仕様変更を積み重ねる方向で分解して出し直すのが良いと思います。(というメモリチェッカー、便利なので知らなかった人は是非使うべき)がメモリリークをたくさん報告してくるんですがという報告です。クラス定義とかは、実際問題スクリプトが生きてる間はずっと消せないわけですので、ようはプロセスが終了するまで放置するより他ないわけです。実際にvalgrindが報告するメモリリークはこれらを検出しているものです。いう種類のメモリ領域はべつにRubyに限らず発生するわけですが、プロセスが終了する時に律儀にfreeして回るべきか、という論点にはfj.comp.lang.c++以来のひじょうに長い議論の歴史があり、少なくともすべき派/すべきでない派それぞれの主張に一理ずつはあるという状況かと思います。実際、よそを見ると、C++11以降ではstd::quick_exit()という関数が追加になり、プロセス終了時に生きてるオブジェクトに対してデストラクタを走らせないモードが選べるようになりました。freeしない派の意見も尊重されつつあるようです。freeすべき派の人がこのスレッドに参戦するのであれば、その理由が「valgrindがうるさいから」だけではちょっと弱いんじゃないかと思いました。JOINが念頭にあるのだと思います。joinに需要があること自体は否定できないものがあります。なんだけど、どうも読んでいるとleftぽくないというか?なんだろう、ちょっと不思議な挙動を目指してるように読めてしまうんですね。joinを投げたらNULLもきちんと左が残るはずなんですが(ですよね?)。提案はそうはなってなさそう。いうわけで、この提案自体は残念な結果になってしまいましたが、それは提案の細部がふんわりしているのが理由なので、leftという発想そのものが否定されているわけではないはずだと思います。練り直して再挑戦して欲しいですね。deprecated?これも浮上してきた古いスレッドで。主張としてはThread.exclusiveは必要なのでなくさないでほしいという話です。なくす側のモチベーションとしては、スレッド間の排他制御は明示的にMutexを利用してやるべきで、グローバルなThread.exclusiveみたいな記述をすべきではないという、哲学というか、そういう行儀の良さを主張していきたい。議論を追いかけていくと、たしかに今、Thread.exclusiveをなくしてしまうと困るシチュエーションが存在しそうです。本当にMutexで明示的にロックをかけるのであればMutexをどこかのタイミングで初期化する必要があるのは当然ですが、Mutexを初期化する部分を保護するMutexが必要という、鶏卵状態がある。ライブラリ作者はライブラリの中で自分が使うMutexを初期化すべきですが、ライブラリをrequireする部分がマルチスレッドで複数スレッドから一斉に呼ばれた時に、複数のスレッドが同時にMutex初期化してしまわないか、どうも怪しい感じになりそうで、そこを保護するためにもグローバルなロックがある方が良い、という主張がされています。その主張には一理あるとは思うのですが、一方でMutexの初期化ごときのためにグローバルに待ち合わせが走るというのはいかにも話が大きすぎるんじゃないでしょうか。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はやりすぎじゃないのというのも分かるんですけどね。cases)ここにきて突如Integer#dupが可能になるというコペルニクス的転換が...そもそもdupというのはオブジェクトのコピペをするときに使うやつなわけですけれども、整数とかいくつかのオブジェクトは最初からimmutableにできているので、コピペがそもそも必要なくて、好きなだけシェアして使えばいい(副作用が何もないから)という理由で、これまではdupなどは例外になるように作られていました。アルゴリズム全体がimmutableに作られている場合が最近は増えてきているのですが、そういう場合だとオブジェクトを壊さないようにdupして回るという場面は結構あります。いうときに、dupできるオブジェクトとできないオブジェクトが入り混じっていると「これはdupできるから〜」みたいな不毛な場合わけが必要になってきて、それはどうなのという話なわけです。本来は「副作用がないから気にするな」の意図だったのに、現状は「例外が起きちゃうから気にしないと」なわけです。いうわけでInteger#dupは、呼ばれると、あたかも正常にコピペされたかのように、しかし実体としてはコピペではなく、それ自身を黙って返すようになりました。String#casecmp?そもそもPOSIXにstrcasecmp(3)という関数が定義されていて、文字列の大文字小文字を区別しない「辞書順比較」を行うものです。?なし)というメソッドがすでにrubyにもあります。ここで、Unicodeという大きな問題があるわけです。文字列の辞書順を決定するには当然、「こちらのほうが後ろ」とか言えないといけないわけですが、一方でUnicodeで大文字小文字を無視した辞書順というのは大変難しい。大文字と小文字の文字数が違う(たとえばU+00DF)とか、小文字コードポイントのほうが大文字よりずんと前に乖離している(たとえばU+212A)とか、いろいろな場合があるので、順序をきちんと実装するにはおそらくUnicodeの全空間の直積で大小関係を定義していく必要があるはずと思います。このような事情によりString#casecmpはUnicodeに対応していません(ASCIIの範囲内でしか動かない)。ところが、ここで問題を「辞書順」ではなくて「等価性判定」に限定して考えると、実は問題の複雑さがかなり緩和される。大文字小文字を変換するのは、複雑とはいえ今回のバージョンのrubyから可能になりました。そこで大文字か小文字かどちらかに寄せてしまえば、その等価性を言うことは可能になってくる。どっちが先でどっちが後かはわからないながらも、違うものは違うとまでは言えるわけですよ。これは問題の切り分けとしてはかなり賢くて、だって、普通あんまり大文字小文字無視した辞書順とかやらないじゃないですか。したいときはだいたいが比較したいだけという場合が多くて、そういう場合向けのメソッドを用意するというのは実用上で有益と思われます。いうわけでtrue/falseを返すString#casecmp?が新設されました。最後にマネーフォワードでは、ruby-coreが気になって夜も眠れないエンジニアを募集しています。ご応募お待ちしています。