JavaScriptの文化とleftpadの話とpadStartについて

無駄にラノベみたいに長いタイトル書いちゃったんですが、まぁやっぱり一言くらいは残しておくかと思ったので書きます。長いのでまとめだけでも見てもらえると良いかもしれません。

leftpadの話はかなり大事になっていて、Node.js界隈を中心としてその他のOSSをやっている全体的に話が波及しています。幾つかの記事を読みました。今回はJSの文化と歴史についてちょっとずつ書いていこうかなと思います。

本の虫: npmからkikとその他諸々が消されたまとめ

江添さんの話はすごくよくまとまっていて、ネタも含めた上で一番面白い話になっていました、ここで言われている下記の疑問に答えていこうと思います。

もっと憂うべきパッケージがある。isArrayだ。このパッケージは一日88万回もダウンロードされていて、2016年2月だけの一ヶ月間に1800万回もダウンロードされていて、72個ものNPMパッケージが依存している。

isArrayの中身はこうだ。

var toString = {}.toString;

module.exports = Array.isArray || function (arr) {
  return toString.call(arr) == '[object Array]';
};

結局、このコードは本質的にたった一行のコードである。

その他、is-positive-integerという整数が正の整数かどうか判定するパッケージがあるが、これも本質的には4行のコードである。ところが、このパッケージは昨日まで3個もの依存を持っていた(今は0個になっている)

なぜこんなにも多数のパッケージに依存をするのだ?

さて、これは JavaScript エンジニアじゃなければ当然の疑問だと思う。
つまり、『isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか』『なぜこんなにも多数のパッケージに依存するのか』である。

isArray のこれまで

さて、行数に注目する前にisArrayについて深掘りしてみましょう。

昔々に存在していた JavaScript GoodParts という古典がある、これについては今日の JavaScript ではほとんどが過去の knowhow になってしまっているが、昔を知る上では非常に重要だと思う。

www.oreilly.co.jp

JavaScript GoodParts にはなんと Array かどうかをチェックする『 is_array という処理はこう書け』という指南が付いている。
「6.5 配列かどうか」より抜粋

var is_array = function (value) {
    return value &&
        typeof value === 'object' &&
        typeof value.length === 'number' &&
        typeof value.splice === 'function' &&
        !(value.propertyIsEnumerable('length'));
};

つまり、値が存在し、値がtypeof で 'object' と判定され、 length というフィールドが数字であり、 splice メソッドを持っていて、 length が列挙可能なものであれば、それは配列であるというわけだ。ここまで判定しないとちゃんとした配列なのかどうかは 昔のJavaScriptでは分からなかった。

さて、この方法が編み出されてから少し時間が経過してさらにカジュアルなチェックが存在するようになった。それが上に貼っていた、Object.prototype.toString.callメソッドの内容で判定する方法だ。

var is_array = function (arr) {
  return Object.prototype.toString.call(arr) == '[object Array]';
};
is_array(['foo', 'bar', 'buz']); // true

さて、そもそも配列かどうかを判定するのにここまで必要な理由がわからないという指摘もあると思う。現代のブラウザでは上のようなチェックも既に過去のノウハウとなっている。今ECMAScript 5 が実装されているブラウザが手元にあれば、これだけで良い。

Array.isArray(['foo', 'bar', 'buz']); // true

JavaScript GoodParts が出版された頃のブラウザの世界では初期のようなチェックまでやらないといけなかったのかもしれないが、現代のブラウザではだいたいArray.isArrayでのチェックをしておけば大丈夫なはずだ。

JavaScriptでArrayの判定 - 思ったこと

isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか

さて、話を元に戻そう、過去を振り返った結果として、最初は複数行あったコードが時代とともに洗練されていくのが分かったかと思う。実はここに『isArray っていう一行のOSSのコードをロードするっていうのはなぜなのか』と『なぜこんなにも多数のパッケージに依存するのか』の理由が隠れている。

JavaScriptの出自がWebブラウザで主に実装されている、という特性上、クロスブラウザでの対応が求められることが多い、その場合に Array.isArray だけでチェックしていた場合、古いブラウザでは動作しなくなってしまう

そこで、 isArray ライブラリの出番になる、Array.isArrayが存在するかどうかを調べて、無かったら内部的に Object.prototype.toString.call() でのチェックをする。こういう『foo機能が存在するかどうかを調べて無かったら内部的に代替手段を用意する』事を polyfillと呼んだりponyfillと言ったりする。polyfill/ponyfillの細かい定義はおそらく誰か別な人が書くと思うのでそちらに任せる。

レガシーブラウザも含めた広い環境で動作することが要求されるアプリケーションであったり、Node.js v0.10でも動くことを求められているライブラリで全て polyfill をわざわざ手書きしていたら生産性が落ちてしまうのは想像しやすいと思う。そこで OSSライブラリに頼ってなんとかするような状況になる。

この polyfill を使う文化 こそがJavaScriptOSSのコードをロードする文化であり、多数のパッケージに依存する原因である。

『たかだか1行とか11行の〜』という話だが、行数自体は関係なく、 polyfill/ponyfill を使って書いていたほうが全体的に整合が取りやすいことが多いから書いていることが多い。もちろん自分でpolyfillを書いたほうがコスト的に安いことも多いので、その場合はわざわざ OSS に頼ったりはしないが、ある程度成熟したpolyfillは自分で書くよりも実績があるのでそれを使っておく方が安心する事も多い。

また、 polyfill というのは言い換えれば『新しい関数が環境に行き渡ったら不要になる』ものだ。

いらなくなったら該当のpolyfillは削除できる。そうして徐々に新しい関数に適用していく事で今日のウェブは急に壊れることなく使えている

翻って leftpadの話

leftpadが unpublish された件 はコミュニケーションのミスや npm の仕様などの不幸が重なった結果発生した事故で、あんまり頻繁に起きるようなことじゃない、とはいえ、かなり大きなニュースになってしまった。もう npm からは今回の反省点も含めてちゃんと声明を出しているので個人的にはnpmの対応には納得している。

The npm Blog — kik, left-pad, and npm

『急に壊れることなく使えている』という話をしたが、今回は不幸が重なって一部のOSSが壊れてしまった。こういう事態を避けたかったら自前で書くほうが良いと言えるだろう、もっと言えば npm に限った話ではなく github であったり bitbucket であったりが急な不幸に見舞われることだって無いとはいえない、それをどうしても避けたいのであれば、もう自分で管理するしか無い気はする。OSSに依存するっていうのはそういう事だと割りきって使っている。

さて、leftpad も isArray と同じようなたった 11行のコードである、頑張れば1行でも書ける。

leftpad のコードがまずいとかそういう話で色々語られてはいたが、確かに愚直にループを回して足していくより今は String.prototype.repeat というメソッドがあるのでそれを使ったほうが綺麗にコードが書けるし、高速である。

paulownia.hatenablog.com

ただ String.prototype.repeat自体が ES2015から新しく定義された関数であり、leftpadのような幅広い環境で動かすような事を目的としたライブラリでは利用できない。最速な実装とは言えないが、単純に書くなら現状維持でも十分問題ないと思われる。

これも ES2015 が十分に行き渡れば中のコードを String.prototype.repeatで書き換えてしまって問題ないはずだ。

padStart の話

話を未来方向に向けるとpadStart/padEnd と呼ばれる仕様が ECMAScript で提案されている。実際のインタフェースはleftpadとは少し違うが下記のように書くことができる。

'hello'.padStart(10); // '     hello'

さらによいニュースとして、実際に v8 では実装が始まっている、ChromeやelectronやNode.jsではこれが使えるようになる事もそんなに先の話ではない。

https://chromium.googlesource.com/v8/v8/+/1a272ba23ec490f73349201c014537c851f3c964

つまりは leftpad のようなコードも時代とともにこう変わっていくだろう。

現在:

function leftpad (str, len, ch) {
  str = String(str);
  var i = -1;
  if (!ch && ch !== 0) ch = ' ';
  len = len - str.length;
  while (++i < len) {
    str = ch + str;
  }
  return str;
}
leftpad('hello', 10);

ちょっと先の未来

function leftpad (str, len, ch) {
  str = String(str);
  if (!ch && ch !== 0) ch = ' ';
  return String(ch).repeat(len - str.length) + str;
}
leftpad('hello', 10);

更に先の未来

'hello'.padStart(10);


さて、 leftpad を padStart の polyfill として見た場合には、 isArray と Array.isArray と同じ状況であることがわかると思う。この手の話はたくさんある。

僕はJavaScript というのは我々言語ユーザーと言語仕様策定者が歩みを寄せながら差を埋めつつ育てていくものだと思っている。polyfillだったり、痒いところに手が届くような小さいモジュールを作ってnpmに公開しておく行為は差を埋める一つの手段である。

そういうライブラリが浸透していくことで言語仕様策定者側はユーザーにとって必要な機能がわかる、ライブラリを元に仕様を決めていくための指針ができる。ライブラリを公開/利用するのは無理矢理言ってしまえば未来のJavaScriptへの貢献にもなっている。

まとめ

  • isArrayの歴史(Goodparts => Object.toString => Array.isArrayの変遷)
  • leftpad も実際はisArrayと同様
  • JavaScriptは言語ユーザーと言語使用者が歩みを寄せて差を埋めつつ育てる言語、polyfillもnpmへの公開もそれを使うのも、その差を埋めるための手段