Write and Run

it's a simple way, but the only way.

Stream#pipeの破棄イベントの伝播

あっさりハマってメモリリークさせまくったので報告。

さっそく本題。まず図を用意。

Readable Stream -(pipe)-> Transform Stream -(pipe)-> Writable Stream

みなさんご存知のデータの流れ。しかし、問題は破棄イベントの伝播方向

Readable Stream が閉じた場合

  1. Readable Stream が閉じる
  2. Transform Stream が閉じる
  3. Writable Stream が閉じる

=> 全部閉じる

よいですね。

Writable Stream が閉じた場合

  1. Writable Stream が閉じる
  2. おしまい
_人人人人人人人人人人人人人人人人_
> Writable Stream しか閉じない <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄

=> 源流側が破棄されない

まとめ

正直、知らんかった(バカ)。

"Stream"について考えていたらConduitを知った

Iteratee ってちょっと古かった感じですかそうですか。今更騒ぎ立ててごめんなさい。

Iteratee わけわかんねーな、run ってなんだよとか思っていたら、Conduit を見つけた。push 型から pull 型へ先祖返り、と。なんだか Stream(1) から Stream2 への移行みたいなことがこっちでも起きてるんだな。

さて、Iteratee について疑問だったことといえば、Enumeratee 同士の結合がなかったことだけど、Conduit では Conduit 同士を結合できるっぽい。ちなみに、流れ的にはこんな感じ。

Source -pull-> Conduit -push-> Sink

Conduit が左から引っ張りだして右に突っ込む感じのよう。まだよくわからん(コードも読んでないから)雰囲気だけど。

ちなみに結合則(っていうのかな?)をまとめるとこんな感じ。(相変わらずのオレオレ記法でごめんなさい)

 Source  $= Conduit := Source
Conduit =$  Sink    := Sink
Conduit =$= Conduit := Conduit

BNF を書いているみたいでとても素敵だ。Conduit を Source と結合しても Sink と結合してもいいあたり、とても美しい。やばい。

詳しくは とか Data.Conduit とか読んで俺も今から勉強します。

"Stream"について考えた(それとIteratee)

タイトルは釣り。どっちかというと Iteratee がメイン。(だってみんな Iteratee とか見ても記事開かないでしょ)

1週間くらい Stream について考え続けて、やっぱりわからなくて。でも、こんな基本的な概念、誰かが既に考えつくしてるに違いない、と思ってググってみたら、あった。そして、またお前か、Haskell

Iteratee

ほとんど Stream。モナドとかオートマトンとか言い始めるときりがないので演算子とその働きだけ大雑把に眺めてみた。

Gist: Iteratee とその演算子の働きとか。すっごく大雑把で、記号も適当ですが雰囲気だけ。

==================================================================
                Enumerator -> Enumeratee -> Iteratee
==================================================================
                                            Iteratee >>== Iteratee
                                         := Iteratee
------------------------------------------------------------------
                Enumerator $$               Iteratee
                                         := Iteratee
------------------------------------------------------------------
                              Enumeratee =$ Iteratee
                                         := Iteratee
------------------------------------------------------------------
                Enumerator $= Enumeratee
             := Enumerator
------------------------------------------------------------------
Enumerator >==> Enumerator
             := Enumerator
------------------------------------------------------------------

Stream に置き換えると、Enumerator が Readable、Enumeratee が Transform、Iteratee が Writable、という感じ。ちょっと違うけど、そんな感じ。

Stream と違うのは、Enumerator 同士の結合と、Iteratee 同士の結合。これが型と相まって地味に便利っぽい。

ということで JavaScript で Iteratee を実装してみてます。どうなることやら。

続: Stream2のモヤモヤ

追記で書くほどの分量でもなくなってきたので別にしました。

まず結論からいうと、

ドキュメントが追いついてない

だけだったようです。このコミット streams: Support objects other than Buffers · 444bbd4 · joyent/node · GitHub での変更が、ドキュメントに反映されてないようです。ドキュメントは the entire buffer とか言ってるけど、ソースコード的には objectMode のときには size 1 なのね……

If we are in "objectMode" mode then howMuchToRead will
always return 1, state.length will always have 1 appended
to it when there is a new item and fromList always takes
the first value from the list.

俺の半日を返せと。

本題

さて、謎の原因がわかったところで本題。

現状の stream2 は buffer, string, object を chunk としてサポートしている。しかし、他の2つとくらべて object は特殊。どれくらい特殊か、下の表にまとめてみた。

length プロパティ 連結の可否 任意の型のデータの格納の可否
string ある concat メソッドで可能 不可
buffer ある concat メソッドで可能 不可
object 意味ない object の連結? なにそれ 可能

圧倒的に浮いてる。同列に並べちゃいけない。それに対して array はどうか。

length プロパティ 連結の可否 任意の型のデータの格納の可否
string ある concat メソッドで可能 不可
buffer ある concat メソッドで可能 不可
array ある concat メソッドで可能 可能

これでしょ。サポートするなら。

ストリームの chunk は joinable で size(length) を持つべきだと思うんです。どこで切ってもよく、どこでつなげてもいい、そういうデータが chunk であるべきだと思うんですよ。

結論

object のサポートなんてやめて、array をサポートすべき。

とはいえ

「過去のモジュールでは object を chunk として流すものも多い。互換性はどうするんだ」とかいう反論があるかもしれない。でも個人的には stream2 は stream と決別して進化して欲しいし「歴史的経緯」なんてもので汚れてほしくない。stream1 は生かしつつ、stream2 が別に生まれるくらいでいいんじゃないかとも思う。

引数なし read() が返すデータのサイズについて

とか言われたのでちょっと考えてみたけれど、適切なバッファサイズとか流量とか、まだよくわかってない……。なのでもう少し流量制限について調べて、触ってみて、なにかわかったらまた記事書こうと思う。

Stream2のモヤモヤ

とりあえず、分かったことだけ。

正しいことを書いてるとは限らないよ!! むしろ昨日はじめて stream2 に触ったからツッコミ待ちだよ!! ドキュメントもサンプルコードも少なくて困ってるよ!! 助けて!!

3つのモード

readable stream には stringMode と objectMode、それと名前はついてないけどいわゆる bufferMode がある。で、stringMode と bufferMode はほぼ同一なのでいいとして、問題は objectMode。

ReadableStream#read([size])の挙動

read() は引数 size が省略された場合、キューの全てを返すことが期待される*1

しかし、objectMode の場合、キューの中から1つだけ返されるだけである。引数なしの read() を一度呼べば全てのデータが吸い出せるという前提でコードを書くとこの仕様にぶち殺される気がする。返り値が null になるまで読めという指摘もあるかもしれないが、キューに null を突っ込むこともできる。あちゃー。

個人的には、read(2) とかで [{obj: 1}, {obj: 2}] みたいになってほしい。つまり、キューから size ぶんだけ配列で返してほしい。そうすれば read() のときにも [{obj: 1}, {obj: 2}, {obj: 3}] になるし、中身に null が入っていても [null, {obj: 1}] となるから区別できる。

この意見の妥当性をもう一度自分で確かめられたら本家にアイデア投げてみようかなーと思う。

追記1

ReadableStream のことしか考えてなかった。pipe するときに WritableStream#write に配列が投げられたらアレゲだった。でも、結合不可なデータをストリームに流すのってどうかと思うから、オブジェクトを write するときは write([obj]) にしろ、っていうのも悪くないんじゃないかと思った。そうすれば複数チャンクを一度に書き込めるし。

まだまだ考えてみようと思う。

追記2

Twitter 上で id:Jxck さんにいろいろ指摘をいただきました。現状の Stream2 に対する理解が深まりました。ありがとうございます!

ログをだらだら貼り付けておくので、まとめだけ読みたい人は下へどうぞ。











以下、自分の勝手な解釈。必然的に間違いを含むはず。(てか正解があるなら今すぐ欲しい感)

「書き込む」というとややこしいので、ReadableStream のキューにデータを追加することを「キューに吸い込む」と呼ぶことにする(便宜上)。

まず、ReadableStream には大きく2つのモードがある。それが、objectMode と非 objectMode。キューにデータを吸い込む際、そのデータが string, buffer, null, undefined のいずれでもない場合、自動的に objectMode になる。一度 objectMode になると二度と非 objectMode にはならない。ストリームは object に「汚染」される。

……と、ここまで書きかけて、chunk と呼んでいたものは buffer で、chunk と buffer には区別があるらしいことがわかってしまったので、もう一度まとめ直したい。

あと、コミットログ追っかけてわかったこともあるので、全部まとめ直します、はい……

追記3

リンク張ってなかったけど、続き書きました→ 続: Stream2のモヤモヤ - Write and Run

*1:参照: http://nodejs.org/docs/v0.9.9/api/stream.html#stream_readable_read_size

Object.observeに関して(あと bind-unit とか)

先日

血迷って どうしてもObject.observeを使いたい場合 - Write and Run なんて記事を書きましたが、再びよくソースを呼んでみたら、setTimeout で polling してました。全然クールじゃない。まぁ、そんなもんだろうとは思っていたけれど。(もちろん、ネイティブ実装ではアクセスフックしてるので高速です。早く標準になれ)

そういえば先日、KOBA789/bind-unit · GitHub なんてものを作りました。こいつはプロパティの new, delete イベントこそ補足できませんが、update ならちゃんと補足できます。ついでに Array を監視する機能も追加予定なので、Object.observe の研究よりそっちを先にやろうかなーと思ってます。というか、今思えば EventEmitter のインターフェース実装する必要なかった気がする。(おかげでいろいろ面倒になってる)

日本科学未来館へ行ってきた

こんばんは。KOBA789 です。今日は「日本科学未来館」へ行って来ました。小学校の遠足で行ったきりなのでだいぶ久しぶりです。

やはり建築の基本に抗って下部が狭く、上部が広くなっている形状の建物は面白くていいですね。内部の展示ももちろんですが、外から眺めていても飽きません。建物自体が1つの展示物のようです。

見たもの

大体全部見ました(適当)。プラネタリウムも見ました。あ、ASIMO のショーだけは見てないかも。で、その中でも一番興奮したものを紹介します。

「インターネット物理モデル」

これ。マジ。

いや、ほんとすごいんですって。メカで(擬似)IP パケットをルーティングするんですよ。しかも、黒と白のボールを用いて 1bit を表す方式で(擬似)IP パケットをまさに手作業で構築して、送信できるんです。ちゃんとコード表も横に置いてあります。そのデータ(白と黒のボール群)はそのままコロコロとレールを転がっていくので、コードを覚えてしまえば流れているパケットを目視で読めます(最後の方は目が慣れてきてだいぶ読めるようになってました)。

日頃からスイッチのアクセスランプ眺めたり Wireshark のログ眺めたりしている人にはたまらないと思います。

また行きたい

合計すると2時間くらいその展示を見ていた気がしますが、まだまだ足らないのでまた行きたいです。日本科学未来館には年間パスポートもあって、1200円(新規・再入会時)と格安です(継続は1000円なので更に安い)。18才以下なら2ヶ月に1度、大人なら半年に一度以上行くと元が取れる計算なので、頻繁にパケットを眺めたい人にはおすすめです。

あと、メカニカルなルーターの構造に関して、光学センサーで先頭 1byte のアドレスを読んでることだけはわかったのですが、上流のルーターに流す部分とかあまりよくわからなかったので、今度行った時にスタッフを質問攻めにしたいところです。

以上、とってもおすすめな日本科学未来館ですので、パケットを眺める趣味がある人もそうでない人も、是非足を運んでみてください。