M12i.

学術書・マンガ・アニメ・映画の消費活動とプログラミングについて

小路幸也『マイ・ディア・ポリスマン』

マイ・ディア・ポリスマン

マイ・ディア・ポリスマン

この人の作品は面白けれども良くも悪くも内容が軽いです。この作品も同じ。そんなこと言ったってどういう作風・どういうテーマを選ぶかは作者の自由。もちろんそれはそうなのですが、主人公たちは家族とか友情とか男性性/女性性とか、そういう規範的なものに無邪気というか屈託がないというか、ともかく何だかんだ言いつつもあっけなく落ち着きどころを見つけ出してしまうのですから、やっぱり軽い。味気ない。そう思ってしまうのです。

興味本位にTransformストリームを実装する

Browserifyのドキュメントを読む中で必然的にNode.jsのAPIについてのドキュメントも参照することとなり、その中でも以前抄訳をしたStream APIに関数するものを読むうちに、試しにTransformストリームを実装してみたくなりました。

Transformストリームの作り方

というわけで2つのストリームを作ってみました:

  1. 文字列の内容を行ごとに分割するTransformストリーム
  2. 行ごとに分割された文字列末尾の改行文字を除去するTransformストリーム

カスタムメイドのTransformストリームは他の種類のストリームと同様にNode.jsの標準ライブラリの助けを借りて簡単に作ることができます:

// 何はともあれStream APIのモジュールを読み込み
var stream = require('stream');
// それを使用してカスタムメイドのTransformを生成する関数を定義
var makeMyStream = function() {
  // Transformコンストラクタを利用して独自の変換処理を行うストリームを生成
  return new stream.Transform({
    /* ここに種々のオプションとともに、
    stream._transform()メソッドの実装となる関数を指定 */
    transform: function(chunk, encoding, callback) {
      callback();
    }
  });
};

1. 文字列の内容を行ごとに分割するTransformストリーム

var stream = require('stream');

// ビジネスロジックで使用する定数
var CR = '\r', LF = '\n';

// Readable#push(any)プロキシを作成する関数。
// Streamとそのサブ型のメソッドは、thisにただしくレシーバが設定されていないと機能しない。
// このためReadable#pushのFunctionインスタンス参照を直接他の関数に渡して
// 後者の関数内で呼び出しを行う、というようなことを行うとエラーとなってしまう。
// このプロキシ作成関数はその引数にthis(that)=レシーバをとり、
// このthisに対してReadable#push(any)呼び出しを行う関数を生成することで問題を回避する。
var pushProxy = function(that) {
  return function(chunk) { return that.push(chunk); };
};
// Readableから文字列を読み取って行ごとに分割するTransform。
// このTransformのWritableの側面はチャンクとしてBufferを消費します。
// 一方Readableの側面はチャンクとしてstringを生産します。
// 分割された行文字列(生産されたチャンク)の末尾にはCR、LF、もしくはCRLFが付随します。
var makeLines = function() {
  // mayPush関数の前回処理で判断保留された「残り」が格納される変数
  var remaining = null;
  // 行分割を行い適宜Readable#pushを実行する関数。
  // 引数として文字列チャンクとReadable#pushを行う関数の参照を受け取り、
  // 必要に応じて行分割を行ってReadable#pushを実行する。
  // 文字列チャンクの終端は行の半ばである可能性があるから(その可能性の方が高いから)、
  // この関数は「文字列チャンク内における最後の行」をエンクロージング・スコープの変数remainingにキャッシュする。
  // キャッシュされた文字列は次回mayPushが呼び出されたときに
  // 新しい文字列チャンクの先頭に連結されて真っ先に処理対象となる。
  var mayPush = function(chunk /* :string */, push /* :(string) => boolean */) {
    // 前回処理で判断保留された「残り」が存在するかどうかチェック
    if (remaining !== null) {
      // 存在した場合は、今回処理の文字列チャンクの先頭に連結する
      chunk = remaining + chunk;
      remaining = null;
    }
    // 文字列チャンク内の現在読み取り中の文字位置を指す添字(i)、
    // 「現在の行」の左端の文字列チャンク内における文字位置を指す添字(left)、
    // 文字列チャンクの全長(len)
    var i = 0, left = 0, len = chunk.length;
    // 文字列チャンクの終端まで1文字ずつ処理
    for (; i < len; i ++) {
      // 現在読み取りi位置の文字を取得
      var ch = chunk.charAt(i);
      // 現在文字がLFかどうかチェック
      if (ch === LF) {
        // LFである場合、即座に行の切り出しを行う
        // 現在位置+1が切り出し対象の右端(exclusive)の添字
        var right = i + 1;
        // 切り出した文字列をReadable#push(string)に渡す
        push(chunk.substring(left, right));
        // 切り出した行の右端の添字を次回処理の行の左端として設定
        left = right;
      } else if (ch === CR) {
        // 現在文字がCRである場合、次の1文字についても判断を行う
        // iをインクリメント
        i ++;
        // 文字列チャンクの終端に達していないかチェック
        if (i < len) {
          // 終端に達していなかった場合
          // 「次の1文字」を取得
          ch = chunk.charAt(i);
          // LFでない場合は読み取り位置をリセット
          if (ch !== LF) i --;
          // 現在位置+1が切り出し対象の右端(exclusive)の添字
          var right = i + 1;
          // 切り出した文字列をReadable#push(string)に渡す
          push(chunk.substring(left, right));
          // 切り出した行の右端の添字を次回処理の行の左端として設定
          left = right;
        } else {
          // 終端に達していた場合は何もせずに即座にループを抜ける
          break;
        }
      }
    }
    // ループ処理後も未処理分として残った文字列を
    // 今回処理で判断保留された「残り」として変数に格納する
    remaining = chunk.substring(left);
  };
  return new stream.Transform({
    // 変換の結果としてチャンクは必ず文字列となるので
    // Readableの側面は objectMode = true
    readableObjectMode: true,
    transform: function(chunk /* :Buffer|string|any */, encoding, callback) {
      // チャンクを文字列化した上で行分割にかける。
      // 1チャンクに対して0〜n回のReadable#push(string)メソッドを呼び出すため、
      // pushメソッドの呼び出しを代行するプロキシを第2引数として渡す。
      // プロキシを介さなくてはならない理由についてはpushProxy関数の説明を参照のこと。
      mayPush(chunk.toString(), pushProxy(this));
      // 処理の完了を示すコールバック実行
      callback();
    },
    final: function(callback) {
      // 行分割を行う関係で最後のtransform(_transform)メソッド呼び出しの後にも
      // 未処理の文字列が残っている可能性がある(その可能性高い)。
      // このためfinal(_final)メソッドでもう1度mayPush関数を呼び出す。
      mayPush('', pushProxy(this));
      // 処理の完了を示すコールバック実行
      callback();
    }
  });
};

module.exports = makeLines;

2. 行ごとに分割された文字列末尾の改行文字を除去するTransformストリーム

var stream = require('stream');

var CR = '\r', LF = '\n';
var mayTrim = function(chunk /* :string | any */) {
  // チャンクを文字列化する
  chunk = chunk.toString();
  // 「最後の文字」の添字を計算
  var last = chunk.length - 1;
  // 添字次第で処理を分ける
  if (last === -1) {
    // -1の場合、対象文字列は空文字列ということ。そのまま返す
    return chunk;
  } else {
    // それ以外の場合
    // 「最後の文字」を取得
    var ch = chunk.charAt(last);
    // CRかどうか判定
    if (ch === CR) {
      // 該当した場合は最後の1文字を除去した文字列を返す
      return chunk.substring(0, last);
    }
    // LFかどうか判定
    if (ch === LF) {
      // 添字がまだ正の数かどうか判定
      if (0 < last) {
        // 「最後の文字」の1つ前の文字を取得
        ch = chunk.charAt(last - 1);
        // それがCRである場合は最後の2文字を除去して返す
        // CRでない場合は最後の1文字を除去して返す
        return chunk.substring(0, ch === CR ? last - 1 : last);
      }
      // 「最後の文字」がLFで、その添字が0ならば空文字列を返す
      return '';
    }
    // 最後の文字がCRでもLFでもないならそのまま返す
    return chunk;
  }
};

// 文字列チャンクの末尾の改行文字を除去するTransformストリーム。
// チャンクとして文字列(stringもしくはStringのインスタンス)以外が渡された場合でも
// Object#toString()により文字列化を強制した上で末尾処理を行う。
var trimEol = function() {
  return new stream.Transform({
    // 文字列レベルの操作が前提となるので
    // Readable/Writableのいずれも objectMode = true
    readableObjectMode: true,
    writableObjectMode: true,
    // Transformストリームのビジネスロジック(_transformメソッドの実装)
    transform: function(chunk, encoding, callback) {
      // チャンクの内容次第で適宜改行文字を除去してからプッシュする
      this.push(mayTrim(chunk));
      // 処理の完了を示すコールバック実行
      callback();
    }
  });
};

module.exports = trimEol;

Transformストリームを使ってみる

何の変哲もないですが、最前作成したTransformストリームを使ってみます:

var fs = require('fs');
var process = require('process');
var makeLines = require('./makeLines');
var trimEol = require('./trimEol');

var fooTxt = fs.createReadStream('./foo.txt', {
  encoding:'utf-8'
});

fooTxt.pipe(makeLines()) // 行ごとに分割する(チャンクの単位も切り替わる)
  .pipe(trimEol()) // 行の末尾の改行文字を除去する(チャンクの単位はそのまま)
  .pipe(process.stdout); // 結果を標準出力に書き出す(改行除去された読みにくいテキストが出力される)

"Browserify Handbook"の翻訳をはじめた

f:id:m12i:20170715235343p:plain

Node.jsモジュールのブラウザ向けバンドル化ツールBrowserifyのドキュメント"Browserify Handbook"。このドキュメントはGitHubで公開されているのでForkした上で翻訳してみることにしました。まずまずの分量があるのでまあ段々とやっていこうかと思います。

はじめに

このドキュメントはモジュール化されたアプリケーションをビルドするためにbrowserify を利用する方法を説明したものです。

browserifyはNode.jsにより拡張された CommonJSモジュールをWebブラウザ向けにコンパイルするためのツールです。

もし仮にバンドル作成とnpmコマンドによるパッケージ・インストール以外では Node.js それ自体を利用していないとしても、あなたはあなたの製造したコードとサードパーティ製のライブラリ群を組み合わせるためにbrowserifyを利用することができます。

browserifyが利用するモジュール・システムはNode.jsが利用するそれと同じです。npm 向けに公開されたパッケージは、それが元来ブラウザのランタイムではなくNode.jsランタイムにおいて利用されることを想定して作成されたものであっても、browserifyによってブラウザ上でも同じように機能します。

多くの人びとがNode.jsランタイム上だけでなくbrowserifyを利用することでWebブラウザ上でも動作するよう設計されたモジュールをnpm向けに公開しつつあります。そしてnpmで公開されている多くのパッケージはまさにWebブラウザ上での利用を想定して設計されるようになってきています。 npm はすべてのJavaScriptランタイムのために利用できるものです。フロントエンドとバックエンドに大きなちがいはないのです。

目次

...