JavaScriptで値を文字列化するにはいくつかの方法が考えられる。 続きを読む
「JavaScript」タグアーカイブ
JavaScript近代史
先月はECMAScript 2015 (ECMAScript 6)が採択され、また、WebAssemblyが発表された。多分これらは現代のWebプログラミング言語の歴史に残る出来事だろう。しかし、ここ10年ぐらいのJavaScript/ECMAScriptの歴史と背景事情みたいのをまとめた文章のようなものをあまり見かけないので、ちょっとだけ書いてみることにする。(というよりは「ちょっとだけ調べてみた」の方が適切。あんまりまとまってないし)
TypeScript 1.5でのlet文
ECMAScriptの変数のスコープが不思議な挙動をするのはよく知られた話だ。ECMAScript 5での変数のスコープは以下のいずれかに大別される(はず)。ブロックスコープがない。
- グローバルスコープ
- 関数スコープ
- catchスコープ
普通に使う変数が関数スコープしかないのは不便、ということで、MozillaはJavaScript 1.7(2006年頃)でlet文を導入した。が、しかし、他のブラウザには普及せず、Mozillaの独自拡張に留まることになった。
時は過ぎ、ECMAScript 5が制定され、次はECMAScript 6という流れになってきた。そして、ECMAScript 6にはlet文が導入されることになった(MozillaがJavaScript 1.7で導入したものとは微妙に仕様が違う)。めでたしめでたし。
ECMAScriptに静的型を付けたTypeScriptは、当初はlet文をサポートしていなかった。それが、TypeScript 1.4で、ECMAScript 6へコンパイルする場合のみlet文をサポートするようになった。
が、ECMAScript 6(のlet文)に対応していないブラウザはまだまだたくさんある(今調べてみたら、IE11ではES6のlet文に対応しているけど、Safariでは対応していないようだ)。できれば、TypeScriptのようなaltJS言語でlet文を使いつつ、コンパイル後のJavaScriptでは従来のvarを使ってほしい。
が、let文を従来のJavaScript (ECMAScript 5)に変換するのはそう自明なことではない。特に問題になるのは、ループ内の変数を、ループの外に持ち出される関数で使っている場合だ。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
let x = i * i;
a.push(function() { return x; });
}
[/code]上のコードを下のように単純にvarを使うと挙動が変わってしまう。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
var x = i * i;
a.push(function() { return x; });
}
[/code]このコードを正しくECMAScript 5に翻訳するには、即時実行関数(IIFE)を使って以下のようにしなければならない。[code lang=”js”]
var a = [];
for (var i = 0; i < 5; ++i) {
(function() {
var x = i * i;
a.push(function() { return x; });
}());
}
[/code]こういう非自明、というかパフォーマンスに影響するところがあるので、TypeScript 1.4ではES5へのコンパイル時にlet文がサポートされなかったのだろう。
が、しかし、TypeScript 1.5ではES5へコンパイルする場合でもlet文が部分的にサポートされるようになるようだ。「部分的に」というのは、ループが絡まない、変数のスコープのチェックと名前の変更で済む場合、のようだ。[code lang=”js”]
{
let x = 123;
console.log(x); // OK
}
console.log(x); // エラー
[/code]
ループが絡む、IIFEを使った変換が必要な場合は、
test2.ts(2,1): error TS4091: Loop contains block-scoped variable 'x' referenced by a function in the loop. This is only supported in ECMAScript 6 or higher.
というエラーが出た。
[code lang=”js”]
let a: Array<() => number> = []
for (var i = 0; i < 5; ++i) {
let x = i*i;
a.push(() => x);
}
a.forEach(console.log);
[/code]
GitHubでの該当するIssue/Pull Requestは以下のようだ。
- Suggestion: Support ES6 ‘let’ and ‘const’ keyword · Issue #19 · Microsoft/TypeScript
- let support for ES3/ES5 · Issue #1690 · Microsoft/TypeScript
- Downlevel emit for let\const by vladima · Pull Request #2161 · Microsoft/TypeScript
let文のサポートとしては不完全とはいえ、varの関数スコープのせいで意図しない同名の変数を参照していた〜〜みたいな事故は防げると思われるので、積極的に使っていきたい。
TypeScript 1.4がいい感じ
TypeScript 1.4がリリースされた。目玉機能としては Union Types とか ECMAScript 6 出力モードの搭載とかになるのだろうが、他にもいろいろ地味だが嬉しい機能追加などがあるようだった。
前にTypeScriptで組み込みオブジェクトを拡張できないというようなことを書いたが、そのとき感じた問題点が直っている。具体的には以下。
- 各種組み込みオブジェクトのコンストラクタの型が
interface ほにゃららConstructor
というものに変更されている。よって、各種組み込みオブジェクトのコンストラクタに勝手なプロパティーを追加できる。 - ECMAScript 6 で追加される関数の型定義が追加されている。つまり、自前で組み込みオブジェクトの型定義をいじらなくていい。(ECMAScript 6 出力モードで使えるようになるようだ)
あと地味に便利だと思ったのはコンパイラ tsc
に追加された --noEmitOnError
というオプションである。今まではソースコードが解釈さえできれば型チェックが通らなくても ECMAScript の出力が生成(!)されていて、Makefile で TypeScript のコードをコンパイルする時に不便だった。だが、このオプションがあれば型チェックが通ったコードしか出力されないので、Makefile との相性が上がる。
TypeScriptで組み込みオブジェクトを拡張できない
最近、自分で書くWebアプリ、ブラウザアプリには、JavaScriptの代替言語としてTypeScriptを使っている。
TypeScriptの気に入っている点は、
- 静的型がついているので、変数名のタイポのような、初歩的だけど実行してみないとわからない面倒なバグをコンパイル時に検出できる
- 文法やセマンティクスがほぼJavaScriptの上位互換なので、すでにJavaScriptを習得している身としては学習コストが低い
- JavaScriptの資産を活用しやすい
- 標準の型定義に入っていないブラウザの拡張API等にも、自分で型定義を書けば利用できる
あたりである。逆に、不満があるとすれば
- セマンティクスがJavaScriptに沿ったものなので、演算子オーバーロードのようなJavaSciptに1:1で翻訳できない機能が使えない
- 型クラスとかがない
- ECMAScript 6で導入される予定の、letや分割代入やジェネレーターなどの、言語の文法に対する拡張に対応していない(新しいラムダ式のような一部の機能は既に導入されているので、将来的には入るかもしれないが)
あたりであろうか。まあ不満があったら他のaltJSを使えばいい話だが。
[2015年1月18日 追記] 以下の内容はTypeScript 1.4のリリースに伴い古くなりました。詳しくはこちら。
ECMAScript 6で導入されるライブラリ関数は、一部は型宣言を書いてやることでTypeScriptからも使えるようになる。例えば、前の記事で Math.hypot
や Math.sinh
などの関数に触れたが、それらは[sourcecode lang=”js”]
interface Math {
hypot(…values: number[]): number;
sinh(x: number): number;
}
[/sourcecode]のような宣言を書いておけば、コンパイルが通るようになる。まだこれらの関数を実装していないブラウザ用に、polyfill(shim)も書いておくか、読み込むといいだろう。
このほか、例えば、Array.prototype.find
の型宣言は[sourcecode lang=”js”]
interface Array<T> {
find(callback: (element: T, index: number, array: T[]) => boolean, thisArg?: any): T;
}
[/sourcecode]というふうにできる。
ここまではいい。
JavaScriptのイディオムとして、配列ライクなオブジェクトを変換するのに、Array.prototype.slice
が使われる。もちろんこのイディオムはTypeScriptでも使えるが、Array.prototype
の型は Array<any>
となっているので、要素の型に対する型チェックや型推論が働いてくれない。幸い、ECMAScript 6では、Array.from
という関数が導入される予定なので、こいつの型を[sourcecode lang=”js”]
from<T>(arrayLike: {[n: number]: T; length: number}): T[];
[/sourcecode]のように宣言してやれば型推論とかが効いて便利なはずだ。
だがしかし。組み込みの Array
オブジェクトの型は標準の lib.d.ts
において[sourcecode lang=”js”]
declare var Array: {
new (arrayLength?: number): any[];
…
prototype: Array<any>;
}
[/sourcecode]のように宣言されている。こいつに from
メソッドを追加しようとして自分のコードで[sourcecode lang=”js”]
declare var Array: {
from<T>(arrayLike: {[n: number]: T; length: number}): T[];
}
[/sourcecode]と書くとエラーが出る。既存のメソッドも書かないとダメかと思って[sourcecode lang=”js”]
declare var Array: {
new (arrayLength?: number): any[];
…
prototype: Array<any>;
from<T>(arrayLike: {[n: number]: T; length: number}): T[];
}
[/sourcecode]と書いてもエラーになる。モジュールならばどうかと思って[sourcecode lang=”js”]
module Array {
from<T>(arrayLike: {[n: number]: T; length: number}): T[];
}
[/sourcecode]と書いてもエラーになる。組み込みの Array
オブジェクトの型は lib.d.ts
で書かれたものから変更できないのだ。
だがちょっと待て。Math
オブジェクトは自前で拡張できたではないか。これは何でだったかというと、Math
オブジェクトは[sourcecode lang=”js”]
interface Math {
…
}
declare var Math: Math;
[/sourcecode]という風に定義されていて、Math
インターフェースにメソッドを追加すれば Math
オブジェクトを拡張できたのだ。Array
も[sourcecode lang=”js”]
interface ArrayConstructor {
new (arrayLength?: number): any[];
…
prototype: Array<any>;
}
declare var Array: ArrayConstructor;
[/sourcecode]と定義されていれば良かったのに。Array
だけではなくて、Object
, Number
, String
等の組み込みオブジェクトも同じ問題を抱えている。
ググってみたらすでに同じ問題で悩んでいる人がいた。
- javascript – Polyfills, Shims & Extensions with TypeScript – Stack Overflow
- javascript – Extending instance/static functions on existing prototypes with TypeScript – Stack Overflow
- TypeScript – View Issue #917: Make class / variable declarations open ended
- Use static interfaces for Ambient declarations in lib.d.ts · Issue #182 · Microsoft/TypeScript
最初のStack Overflowの回答によると、「自前の lib.d.ts
を用意しろ」らしい。つらい。
[2015年1月18日 追記] 以上の問題点はTypeScript 1.4のリリースにより解決しました。詳しくはこちら。
滑らかな曲線をベジエ曲線で近似する
目次
ベジエ曲線とは
ベジエ曲線とはWikipedia(英語版)によると(本当はちゃんとした文献を当たるべきなのだろうが)、\(n+1\) 個の点 \(x_0, x_1, \dots, x_n\) が与えられた時に\[
B(t)=\sum_{i=0}^{n}\binom{n}{i}t^i(1-t)^{n-i}x_i, \quad 0\le t\le 1
\]で定まる曲線らしい。始点 (\(t=0\)) は \(x_0\)、終点(\(t=1\))は \(x_n\) である。\(n\) のことを次数という。
(1次の場合は単なる線分なので置いておいて)よく使われるのは2次の場合と3次の場合である。それぞれ\begin{align*}
\mathit{quadratic B\acute{e}zier}(t)&=(1-t)^2x_0+2t(1-t)x_1+t^2x_2, \\
\mathit{cubic B\acute{e}zier}(t)&=(1-t)^3x_0+3t(1-t)^2x_1+3t^2(1-t)x_2+t^3x_3
\end{align*}で与えられる。
2次の場合は始点 \(x_0\) と終点 \(x_2\) の他に1個の制御点 \(x_1\)、3次の場合は始点 \(x_0\)、終点 \(x_3\) の他に2個の制御点 \(x_1\), \(x_2\) によって決まる。制御点は始点及び終点における接線を与えるためにある。ここではベジエ曲線についてはこれ以上突っ込んだ事は取り扱わない。
こういう、「制御点を与えて曲線を描く」のは、ドロー系の描画ソフトウエアを使ったことのある方はおなじみだろう。SVGやPostScriptなどのベクター画像形式でも、ベジエ曲線は基本的な描画対象である。
プログラミングに関して言うと、大抵のグラフィックAPIには2次か3次のベジエ曲線を描画するAPIがある。例えば、HTML5 Canvasの場合は
context.quadraticCurveTo(cpx, cpy, x, y) /* 2次 */ context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y) /* 3次 */
というAPIがあり、Cocoaの場合は
NSBezierPath -(void)curveToPoint:(NSPoint)aPoint controlPoint1:(NSPoint1) controlPoint2:(NSPoint)controlPoint2 /* 3次 */
というAPIがある。いずれも始点を指定する引数がないが、これらのAPIを使った時の始点は「現在の点の位置」になる。つまり、最後の描画操作における終点か、あるいはmoveToなどのAPIを使って指定した点である。
滑らかな曲線をベジエ曲線で近似する
さて、滑らかな曲線 \(f\colon[0,1]\to\mathbf{R}^2\) をベジエ曲線で近似するということを考えよう。動機としては例えば、プログラミングで曲線を描きたい時に、大抵のグラフィックスAPIには任意の曲線を描画するようなAPIはないので(2次か3次の)ベジエ曲線を使って近似する事になる。2次か3次と書いたが、以後3次のベジエ曲線で近似する事を考える。
ECMAScriptでのいくつかの数学関数の実装
C言語をはじめとする多くのプログラミング言語には、exp
, log
, sqrt
などの指数、対数、平方根の関数や、 cos
, sin
, tan
などの三角関数、cosh
, sinh
, tanh
などの双曲線関数が備わっている。これらの数学関数はECMAScript (JavaScript)からは、Math
オブジェクト経由で Math.exp
や Math.cos
などのようにアクセスできる(が、現状ECMAScriptには双曲線関数はない)。
さて、C99でこれらに加えて、acosh
, asinh
, atanh
などの逆双曲線関数や、expm1
や log1p
などの「精度を意識した」関数などが追加された(expm1
は\(\exp x-1\), log1p
は \(\log(1+x)\) を計算する関数である)。これらの関数をECMAScriptで使うにはどうすればいいか?
ECMAScriptの次期標準であるECMAScript 6では、双曲線関数 Math.(cosh|sinh|tanh)
や、C99で追加された関数に対応する Math.(acosh|asinh|atanh)
や Math.expm1
, Math.log1p
が入るらしい(Firefoxなどでは既に実装されている)。しかし、ECMAScript 6に非対応の環境では、これらの関数は自前で実装するしかない。
幸い、双曲線関数は指数関数で書ける。よって、例えば Math.sinh
はECMAScriptに既に存在する Math.exp
関数を使って次のように書ける:
[sourcecode lang=”js”]
Math.sinh = Math.sinh || function(x) {
return (Math.exp(x)-Math.exp(-x))/2;
};
[/sourcecode]
Mozillaのサイトにも代替コード(Polyfill)として同じようなコードが載っている。めでたしめでたし。
といいたいところだが、精度を気にする場合はそうはいかない。例えば、\(x\) にとても小さい数、例えば \(x=10^{-20}\) を代入してみるとどうなるだろうか。\(\sinh\) のテーラー展開\[
\sinh x=x+\frac{x^3}{3!}+\cdots
\]を考えると(\(x^3\) は微小なので無視して)値はおおよそ \(x=10^{-20}\) と一致するはずである。この場合、ECMAScriptの「数」はIEEE754の64bit浮動小数点数で、精度は53bitしかないため、\(x\) が十分に小さければ \(x+\frac{x^3}{3!}+\dots\) は完全に \(x\) と一致する。
一方、指数関数のテーラー展開は\[
\exp x=1+x+\frac{x^2}{2!}+\cdots
\]で、今書いたのと同じ理由で Math.exp(x)
や Math.exp(-x)
は完全に 1 と等しくなる。従って、さっき実装したオレオレ Math.sinh
は0を返す。情報落ちだか桁落ちだか忘れたが、なんかそういう現象が起こっている。これは理想的とはいえない。ちなみに、C言語の sinh
はこの場合「正しい」 \(10^{-20}\) を返す。
同様に、Math.expm1
を次のように愚直に実装しても、同じ問題が発生する。
[sourcecode lang=”js”]
Math.expm1 = Math.expm1 || function(x) {
return Math.exp(x)-1;
};
[/sourcecode]
では、どうすれば \(x\) が小さい時でも「正しい」答えを出す expm1
関数を作れるか?(双曲線関数よりも指数関数の方が単純なので、以後こちらを議論する)
まあ単純な答えとしては、 \(x\) が小さいときに関数のテイラー展開を使って多項式として計算してやれば良い。さっきのように \(x=10^{-20}\) とかだと1次近似で \(x\) そのものを返してやればよかったが、この近似は \(x\) がどのぐらい小さければ問題ないのか?あるいは、近似の次数をどのぐらい増やしてやれば誤差はどれぐらい小さくなるか?
仮に、\(\exp x\) を \(n\) 次の多項式で近似するとしよう。すると、真の値(無限級数で表される)との差は\[
\left\lvert(\text{差})\right\rvert=\left\lvert\exp x-1-\sum_{k=1}^{n}\frac{x^k}{k!}\right\rvert=\left\lvert\sum_{k=n+1}^{\infty}\frac{x^k}{k!}\right\rvert
\]となる。これを適当に評価してやれば良い。実際にやってみると、例えば次のようになる。\begin{align*}
\left\lvert(\text{差})\right\rvert=\left\lvert\sum_{k=n+1}^{\infty}\frac{x^k}{k!}\right\rvert
&\le\sum_{k=n+1}^{\infty}\frac{\lvert x\rvert^k}{k!} \\
&=\frac{\lvert x\rvert^{n+1}}{(n+1)!}\sum_{k=0}^{\infty}\frac{\lvert x\rvert^k}{(n+2)\cdot\dots\cdot(n+k+1)} \\
&\le\frac{\lvert x\rvert^{n+1}}{(n+1)!}\sum_{k=0}^{\infty}\frac{1}{(n+2)^k} \quad (\left\lvert x\right\rvert <1\text{を仮定}) \\
&\le\frac{\left\lvert x\right\rvert^{n+1}}{(n+1)!}\frac{1}{1-\frac{1}{n+2}} \\
&\le\frac{\left\lvert x\right\rvert^{n+1}}{(n+1)!}\frac{n+2}{n+1}=\frac{(n+2)\left\lvert x\right\rvert^{n+1}}{(n+1)(n+1)!}
\end{align*}
では、どういうときに \(n\) 次の多項式近似の値を使ってよいかというと、近似値 \(\sum_{k=1}^{n}\frac{x^k}{k!}\) の大きさに対してこの \(\left\lvert(\text{差})\right\rvert\) の大きさが(精度が53bitの浮動小数点数の場合は) \(2^{-53}\) よりも小さければ確実に問題ない(十分条件)。つまり\begin{align*}
\left\lvert\frac{(\text{差})}{\sum_{k=1}^{n}\frac{x^k}{k!}}\right\rvert<2^{-53}
\end{align*}だ。この左辺をもうちょっと分かりやすい形で評価すると
\begin{align*}
\left\lvert\frac{(\text{差})}{\sum_{k=1}^{n}\frac{x^k}{k!}}\right\rvert&\le\left\lvert\frac{(\text{差})}{x}\right\rvert \\
&=\left\lvert\frac{1}{x}\frac{(n+2)\left\lvert x\right\rvert^{n+1}}{(n+1)(n+1)!}\right\rvert \\
&=\frac{(n+2)\left\lvert x\right\rvert^n}{(n+1)(n+1)!}
\end{align*}
となる(書いた後に気付いたが、最初の行の評価が \(x<0\) のときにマズい気がする)。つまり、\(n\) 次近似を使っても良い十分条件として\[
\frac{(n+2)\left\lvert x\right\rvert^n}{(n+1)(n+1)!}<2^{-53},
\]すなわち\[
\left\lvert x\right\rvert<\sqrt[n]{\frac{(n+1)(n+1)!}{n+2}2^{-53}}
\]が得られる。
例えば、\(n=3\) として右辺を計算すると \(1.286976\ldots\times 10^{-5}\) が得られるので、さっきのオレオレ Math.expm1
の実装は
[sourcecode lang=”js”]
Math.expm1 = Math.expm1 || function(x) {
if (Math.abs(x) <= 1.28e-5) {
return x*(1+x/2*(1+x/3));
} else {
return Math.exp(x)-1;
}
};
[/sourcecode]
のように改良できる。
まあ、改良したと言っても所詮お遊びで、実際のC言語のライブラリの実装はこんな適当なコードよりもちゃんとやっているので、真面目にやりたい人はC言語のライブラリの実装を移植しよう。
Math.expm1
の他に、Math.log1p
, Math.sinh
, Math.tanh
, Math.asinh
, Math.atanh
あたりの関数を実装する時にも同じような(小手先の)テクニックが使える。
ところで、MDNに載っている Math.tanh
のPolyfillに問題があるのを見つけた。MDNのPolyfillでは x
が Infinity
かどうかを関数の最初でチェックしているが、x
が Infinity
でなくても Math.exp(x)
が Infinity
になる場合を考慮していない。その結果、Math.tanh(x)
に 1 を返してほしいのに実際には NaN
が返ってくる場合がある。
最後に全く関係ない話だが、プログラミング言語Luaは標準ライブラリに math.sinh
などの双曲線関数を計算する関数(中ではC言語のライブラリ関数を呼び出す)を持っている。しかし、どうやら将来的にはこれらの双曲線関数を標準ライブラリから外すようだ(次期バージョン5.3でdeprecated扱い)。理由として作者は「あまり使われていない」「必要なら mathx などの外部ライブラリを使えば良い」としている(この記事に書いた理由により、「math.exp
で実装できる」は理由にならない)。ECMAScript と Lua はよく似ていると言われる事が多いが、一方がWebの世界のアセンブラとなるべく数学関数・浮動小数点数関係の関数を追加しようとしているのに対し、もう一方はそれを削減しようとしているのは対照的だと思った。(まあ筆者自身はLuaを活発に触っていたのは5.1時代の事で、5.2が出て以降はあまり触っていないので、ぶっちゃけどうでもいいのだが)