(Babel 5における)ES6のモジュールを解説してみた
警告
以下でのモジュールの説明はトランスパイラであるBabel 5,6で動作を確認した振舞いについての記述です。2015年11月現時点で、ECMAScriptのモジュール仕様策定範囲は、本来の全体範囲のまだ一部であるとのことです。その状況でのBabelの実装は、良く言えば先行的、悪く言えば将来そのままである保証はなく、現時点でも他のES2015をサポートする処理系との間での相互運用の保証はありません。また、現時点でBabelのモジュール機能を使うこと自体にリスクがあるという意見もあります。CommonJS側からBabelが生成したモジュールをCommon JSモジュールとして読み込もうとしたときの互換の問題として、Babel5で可能だったことがBabel6では利用不可になる、といったことも起きているようです。
そこらへんを含めて解説されているこちらの資料が参考になります。
(2015/11/18追記)
- 警告
- はじめに
- 予備知識
- exportとimport
- モジュール利用の流れ
- export編
- 具体的にはどんな値が返るのか?
- import編
- 「named系」の流れと「default系」の流れ
- 例をベースにした説明
- トピックス
- まとめ
- 参考
はじめに
ES2015(以降、ES2015はES6と書く。長いので)のモジュール機能を説明する。
ES6のモジュールはNode.JSとかCommonJSのそれと近いので、それらの使用経験があれば類推できる部分が大きい。しかし、「Node.JSのモジュールの使用経験がない人」にとってはつらいかもしれない。なので、前提知識をなるべく必要としない形でES6モジュールの用法についてまとめてみる。
予備知識
JSにおいてモジュールとは何だったか
もともとJSに言語仕様としてのモジュール(Javaで言うpackage/import、rubyで言うrequireなどで実現される機能)はなかった。では、従来のJSでモジュール管理はどういうものだったかというと、.jsという拡張子を持ったテキストファイルから文字列を読み込んでevalっぽいもので評価して中でexportされているオブジェクトを識別子にバインドするような処理で実現されていた。ちなみに、インポートされるシンボルで使用されるピリオド(.)は、名前空間の区切りとかではなく、objectのプロパティを実行時にたどっている。
ES6においてモジュールとは何か
前述した、モジュール機構を実装する既存のライブラリの呼び出しに対するシンタックスシュガー的なものを、ECMAScript言語仕様できっちり規定して、将来に渡っても実装に左右されないようにした。内部機構を隠蔽することにもなっており、非同期読み込みなどの効率化を可能としたり、静的解析や事前コンパイルなど最適化を可能としたり、型チェックなどにも寄与できるなど利点が大きい。ES6のモジュールは、ES6の価値の主要なものの一つである。とはいえ、現状では、ブラウザネイティブ始め、まだ実装されていない処理系も多く、トランスパイラ実装では全ての利点が享受できるわけではないが、とりあえずトランスパイラBabelだけで使えれば良く、Babelを使い続ける限り将来互換もなんとかなるやと楽観的な人は使おう。なお、React界隈ではサンプル含めBabel前提でモジュールが良く使われている印象を持っている。(2015/11/18追記)。
exportとimport
ES6モジュールを実現する構文は、exportとimportの2つの宣言である。
- export
- 使用される側のモジュール中で使用し、「そのモジュールで定義している値やオブジェクトのうち、モジュール外から、モジュールを利用する側からどれを利用可能とするか」を指定する。
- import
- モジュールを使用しようとするコード側で以下を指定する
- どのモジュールを使うかの指定
- 指定モジュール内でexport指定されている何かを、どのように識別子にバインドするか
これらはCommonJSやNode.jsのrequireの仕組みとは異なり静的である。importはトップレベルでしか使用できないし、JSコード末尾で使用しても先頭で宣言されたのと同じ効果をもたらす。これによりモジュール間の依存関係が静的に定まり、分割やリロードなどで様々な利点をもたらす。ただしトライスパイラ実装ではこの利点が全て生かせるわけではない(2015/11/18追記)。
ブラウザ上で動作するJSにとって、モジュールのimportは、従来までJSを分割してそれらを個々にHTMLで<script type="javascript" src="">と個別指定していたことの代替になる。ES6を利用するのにトランスパイラを用いたとき、importはBabelやwebpackなどでいろいろ処理されて、きっと最後は「bundle.js」などといった単一のJSとしてブラウザからは見えるようになる。
モジュール利用の流れ
JSにおけるモジュールの利用は、以下の2つの段階に区切って考えるとわかりやすい。
- (1)定義しようとするモジュール側で、外部に提供したい値の集合やオブジェクトにexport指定することで、値やオブジェクトを「返す」ように定義し、
- (2)importの構文で指定された方法で、その「返された何か」を識別子に適切にバインドする。
(1)の段階でのモジュールからの値の返し方には、以下の3つがある。
- named export: 複数値をobjectで返す
- default export: 単一値を返す
- named exportと, default exportの混在: 単一値と複数値の両方を返す
以降、値がどう返されるかをそれぞれ見ていく。
export編
1. named export: 複数値をobjectで返す
export var foo = "abc" export const bar = "def" export function hoge() { return "ABC" };
この場合、内部的には(Babelの仕様としては)
{ foo: "abc", bar: "def", hoge: function(){ return "ABC" }, __esModule: true }
というようなobjectを返す。exportは、個々の変数定義の箇所ではなく、以下のようにまとめて指定することもできる。
var foo = "abc" const bar = "def" function hoge() { return "ABC" }; export { foo, bar, hoge }
ちなみに {a, bar, hoge}
はES6で導入された記法で、{a:a, bar:bar, hoge:hoge}
の略記法と解釈できる?
以下のようにasでリネームしたものを公開することもできる。
var foo = "abc" const bar = "def" function hoge() { return "ABC" }; export { foo as X, bar as Y, hoge as Z }
2. default export: 単一値を返す
export default class MyClass { // 名前のあるクラス foo() { return "FOO" } }
この場合、関数値としてのMyClass関数を単一値として返す。
export default class { // 無名クラス foo() { return "FOO" } }
この場合、関数値としての無名クラスを単一値として返す。
export default 123
この場合、値123を単一値として返す。
export default {indent:1}
この場合、objectを単一値として返す。
上記のように、default exportでは任意の型の値、クラスや数値、objectなどを返すことができる。返すことができる値は単一値であるから1つだけである。export defaultが1つのJSファイル中に複数あれば、ES6の仕様上は本来はSyntax Errorであるが、現在のBabelの振舞いとしては(2015/11/18追記)、後勝ちで上書きされるようである。
3. named export/default exportの混在: 単一値と複数値の両方を返す
export default class MyClass { foo() { return "FOO" } } export var foo = "abc" export const bar = "def" export function hoge() { return "ABC" };
上では、関数値としてのMyClass関数を単一値として返すのに加え、foo,bar,hogeを複数値として表現するobjectを返す。
具体的にはどんな値が返るのか?
Babelでコンパイルしたモジュールでは、モジュールの返り値は常にobjectであり、単一値はプロパティ"default"の値として返る*1。
{ "default": 単一値, // default exportされた値 複数値1の名前: 複数値1, // named exportされた値1 複数値2の名前: 複数値2, // named exportされた値2 複数値3の名前: 複数値3, // named exportされた値3 __esModule: true }
つまり、単一値のexportは、複数値の特別な場合として実現されていて、exportされる単一値というのは、実際には複数値として返されるobjectの"default"というプロパティの値のことである。"default"プロパティの存在は、import文のシンタックスシュガーで隠蔽される。
importしたモジュールをexport
export {x} from "mod"; export {v as x} from "mod"; export * from "mod";
説明は略。
import編
import文について説明する。import文は単一値を受けとるか、複数値を受けとるか、その両方を受けとるか、に応じた4パターンがある。
項番 | 書式 | 例 | 対象モジュール |
---|---|---|---|
1 | import 単一値を受けとる識別子 from "モジュール指定" | import A from "module" |
named exportが使用されたモジュール |
2 | import 複数値を受けとる指定 from "モジュール指定"
|
|
default exportが使用されたモジュール |
3 | import 単一値を受けとる識別子, 複数値を受けとる指定 from "モジュール指定"
|
|
named exportとdefault exportの両方が使用されたモジュール |
4 | import "モジュール指定" | import "module" | 副作用を期待するモジュール |
importにおけるモジュール指定の方法
from句でモジュールを指定する。呼び出す側のJSファイルと同じ、もしくはサブディレクトリや上位ディレクトリに配置されているJSファイルで定義されているモジュールを読み込むには、「./」や「../」で始まるファイル名の相対指定で指定すればよい(拡張子不要)。なおNode.jsではnode_modules配下のモジュールは「./」では始めなくてよい。この仕組みはNodeの仕様だと思うが、ES6でも定義された仕様なのかは知らない。
「named系」の流れと「default系」の流れ
結局、JSのモジュールの使用は、named系とdefault系の独立した2系列があり、以下の区別である。
- named系
- モジュールは名前と値のペアの集合を返す。named importを通じて、元の名前をそのままもしくはasでリネームして使用する。
- default系
- モジュールは単一値を返す。default importを通じてimportする側が新たに名前をつけ、元の名前を意識せずに使用する(名前は元々無いかもしれない)。もっとも、単一値としてobjectを返すこともできるので、スコープ的に使用するobjectを返して、そのプロパティの名前を使うということもできるし普通である。
- named/default混在系
- jQueryの$やjQueryのようにメインのオブジェクトが単一でリネームで衝突回避できるようになっていて欲しいためにトップレベルのメインオブジェクトをdefault exportにして、その他を選別的にnamed exportするケースなどが想定ユースケースの一つ。
図にまとめるとこうである。
ちなみにCommonJSで定義されているのはnamed系に対応する方式(exportsのプロパティに代入)だけで、default exportの用法に近い方式(module.exportsに代入)はNode.JSの機能らしい。
使い分け
default exportでもobjectを返せば名前と値の複数値を返すことができるし、named exportでも1つのキーで単一値を返せる。なので両者の機能範囲は実は重なる。
実際、いずれの形式でも使用できるように提供されているライブラリもある。例えばReactがそうである:
import React from "react" export default class Hoge extends React.Component { }
import {Component} from "react" export default class Hoge extends Component { }
前者だと、常に修飾して使用するので使用箇所でやや煩雑だが、わかりやすいかも。Javaで言うFQCNで使用するイメージ。
後者だと、使用するクラスやシンボルが増えるたびに列挙を増やしていかなければならないので面倒ではあるし、修飾無しなので、衝突の恐れもある。babelはnamed import時の名前衝突をエラーにしてくれるので都度asをすれば良い、という考えもあるかもしれないが、アドホックにたまたま衝突したことを理由として名前を変え、さらにどう変えるべきかを考えるのはうれしいことではない。メリットとしては、ソース冒頭を見るだけでこのコードはどんなクラスを使っているかがわかる。
JSの仕様策定者は、default exportをより推奨したいと考えていて、記法もより簡潔にしたとのこと。
例をベースにした説明
以下を試すには、babelを入れて(npm install -g babel)、babel-nodeで実行していくのが手軽です。 なお、Babel 6ではbabel-nodeは「babel-cli」のnpmに分離されたのでインストール(npm install -g babel-cli)が必要です。また、ES6の使用にはpreset es2015が必要です(npm install babel-preset-es2015して--presets es2015を指定)。ちなみに、Babel 6.1.18ではREPLモードでの実行ではモジュール機能は利用できなくなったようです(使用するとSyntaxError: repl: Modules aren't supported in the REPLというエラーメッセージが表示される)。(2015/11/18補足追記)
1. 複数値のimportの例
named export、すなわちモジュールが複数値で値を返す場合、その値は具体的にはobjectで返されている。値はobjectのプロパティ名に対応する名前を持つ(named exportとも呼ばれる所以)。
(例1-1)
// module1.js export const x =1 export var y = [1,2,3]
import {x, y} from "./module1" console.assert(x==1) console.assert(y.toString()=="1,2,3")
表記上は、モジュールが返すオブジェクトが{x,y }に分配代入されるイメージだが、実際の分配代入ではない(ネスト等はできない)。 named exportでは、import時に指定する識別子x,yは実際にモジュールでexportしている名前と一致している必要がある。一致していない(実際にはexportされていない)変数名を指定するとundefinedとなる。
(例1-2)
// module2.js export const a = 1 export var b = 2
import * as X from "./module2" console.assert(X.a==1) console.assert(X.b==2)
モジュールが返すオブジェクトがXに代入される。Xは任意につけてよい名前である。
(例1-3)
// module3.js export const x = 1 export var y = [1,2,3]
import {x as foo, y as bar} from "./module3" console.assert(foo==1) console.assert(bar.toString()=="1,2,3")
右辺がobjを返すとして、以下のイメージ(実体は少々違う)。
foo = obj.x bar = obj.y
2. 単一値のimportの例
default export、すなわち単一値を返すモジュールはimport x from ..のように指定するとその単一値がxに保持される。
(例2-1)
// module4.js export default 1
import x from "./module4" console.assert(x==1)
module4の返す値がxに代入される。xは任意につけてよい名前である。
3. 複数値、単一値両方の混在の例
単一値と複数値の両方を返すモジュールからの値を以下のように受けとることができる。foo
には単一値が代入され、{x, y}
には複数値が分配される。
(例3-1)
// module5.js export default 1 export var x = 2 export var y = "abc"
import foo,{x,y} from "./module5" console.assert(foo==1) console.assert(x==2) console.assert(y=="abc")
fooは任意につけて良い名前で、x,yはmodule5でexportされた名前と一致している必要がある。
トピックス
defaultとnamedが一致しなかったら?
default exportの指定のみしかない単一値のみを返すモジュールを、named importすると、例えば {foo, bar} fromでimportすると、foo, barはundefinedとなる。
named exportの指定のみしかない、複数値を返すモジュールを、default importすると、例えば X from ..でimportすると、Xはundefinedになる。
ここらへんや、次の項目の振舞いは、「単一値のexportは複数値の"default"プロパティで実現されている」ことの帰結である。
import * as Xとimport Xの違い
import * as X from "..."
は、named系であり、named exportされたモジュール(複数値を表現するobject)の指定を期待している。「ばらばらで来たものをまとめる」というイメージ。もしfromにdefault exportされたモジュールのみが指定されれば、Xには{default: 単一値 }というオブジェクトが得られる。
import X
はdefault系であり、default exportされたモジュール(単一値を表現している)を期待している。「単一値ならそのまま、まとまって来たものなら後でバラしてつかう」というイメージ。もしfromにnamed exportされた複数値を表現するobjectのみを返すモジュールが指定されれば、Xにはundefinedが得られる。
まとめ
ES6のモジュールは良く考えられてうまくできているよー。
参考
- http://d.hatena.ne.jp/jovi0608/20111226/1324879536
- http://www.2ality.com/2014/09/es6-modules-final.html
- http://www.slideshare.net/teppeis/es6-in-practice
オライリージャパン
売り上げランキング: 22,622
技術評論社
売り上げランキング: 196,459
売り上げランキング: 1,140
Reactはリアクティブプログラミングなのか?
Reactとは
Reactは、Facebookが開発した、JSのUIフームワークもしくはライブラリです。Reactが提供する中核機能は以下です。
- イミュータブルなUIビルダー
- Virtual DOMによる効率的更新
- 上記に付随するイベントハンドラ群を編成していくための方法論
- React単体ではコールバックの組合せで、Fluxの一部として使用するとオブザーバーパターンで実現
効用は、再利用性と保守性・可読性向上です。特に、Reactで作成した画面部品のコンポーザビリティが高く、細粒度のUI部品利用の発展充実が期待されます。作りは通常のJSクラスライブラリであり、覚えたりすることは多くありません。
設計をとりもどす
Reactが解決しようとする問題は、大規模化するSPAにおいて、大域状態をDOM内の値として管理し、無数のイベントハンドラがただあるだけ、といった「設計不在」への対処です。Reactは「UIはこう設計しようぜ」という導きであり、設計を大事だと思う一部の人には大受けします。やんややんや。
Reactでやってみる
さて、以前、以下の記事では、関数型リアクティブプログラミング(FRP)を実現するAlt JSであるElm言語を用いて、マウスストーカーというマウスを追い掛ける★を表示するプログラムを実装しました。
上記の元になった記事は、Bacon.jsで実装した記事なのですが、ElmもBaconJSもリアクティブプログラミングを実現する技術です。ElmのHtmlライブラリ「Elm-html」はReactと同様にVirtual DOMをレンダリング効率化技術として採用しているので、Reactだとどうなるかなと思ってやってみました。
以下がReactでのマウスストーカーの実装です。Resultsで実行できます。
画面キャプチャは以下です。
コードは以下になります。
'use strict'; var React = require('react'); var ReactDOM = require('react-dom'); var Star = React.createClass({ getInitialState: function() { return {x:30, y:30} }, setPosition: function(x, y, refs) { this.setState({x:x, y:y}) if (this.props.followedBy != null) { setTimeout(()=>{ refs[this.props.followedBy].setPosition(x, y, refs) }, 100); } }, render: function() { return ( <div style={{position:'absolute', left:this.state.x, top:this.state.y, color:'orange'}}> ★ </div> ); } }); var Screen = React.createClass({ onMouseMove: function(ev) { this.refs._1.setPosition(ev.clientX, ev.clientY, this.refs) }, componentDidMount: function() { document.addEventListener('mousemove', this.onMouseMove); }, componentWillUnmount: function() { document.removeEventListener('mousemove', this.onMouseMove) }, render: function() { return <div> <Star ref="_1" followedBy="_2"></Star> <Star ref="_2" followedBy="_3"></Star> <Star ref="_3" followedBy="_4"></Star> <Star ref="_4" followedBy="_5"></Star> <Star ref="_5" followedBy="_6"></Star> <Star ref="_6" followedBy="_7"></Star> <Star ref="_7" followedBy="_8"></Star> <Star ref="_8"></Star> </div> } }); ReactDOM.render(<Screen />, document.getElementById('container'));
Elm-HtmlとReactの対応
Elm版では、DOMベースではなくてElmのCanvasベースのAPIを呼んでいたので、本当の比較は実はできません。なので「Elm-Htmlで作っていたら」という想定をした上での比較になりますが、以下のとおり。
Elm-Html | React |
---|---|
Signalに包まれた値 | Reactコンポーネントに包まれたstate |
SIgnal Htmlを返す関数 | stateを持ったReactコンポーネントのrender() |
Signalには包まれないHtml値を返す関数 | stateを持たないReactコンポーネントのrender()。実際、stateを持たないreact componentは、React 0.14ではクラスではなく関数として書ける。 |
純粋関数でのビュー構築 | コンポーネントAPIのrenderメソッド |
mainでの、純粋関数をSignal.map*してシグナル値を当ててSignal Htmlを得る操作 | ReactDOM.renderComponent() |
on*で設定したイベントハンドラからport経由でSignalをキック、そしてそのSignalに結びついたfoldpが起動されるまで | コンポーネントでの各種イベントハンドラを実行してthis.setState() |
Elm Archtecture | ルートコンポーネントのみにstateを置くという方針 |
- | flux |
Singalの合成や加工操作(Signal.sampleOn, Time.delay,..) | なし |
このように対応付けることができます。一見すると、両者はとても似ているとも思えます。しかし、その類似性の多くは、両者ともVirtual DOMを採用し、さらにやっている処理が同じであることに由来しており、必然です。最終的にはJS上で、同じようにDOMイベントを処理して、同じDOMを描画する以上、対応付かないはずがないのです。
なので差異の方が自分としては興味深いところです。たとえば、Elmでは純粋関数しかないので「直接Signalを発行できない(setStateできない)」という制限が大きく、Reactではハンドラ内のsetState()で済むところをport経由でTaskを起動したりしないとなりません(MessageやAddressを経由した暗黙にせよ)。
また、ElmではTaskの定義場所と、そのタスクをキックするハンドラなどがポートを介してばらばらになりますが*1、両者を一つのクラスとして書いて対応付けるReactの方が私にはわかりやすく感じました。
ReactはFRPか?
まず、FRP(関数型リアクティブプログラミング)の定義ですが、こちらを参考にすると、「動的であり変化する値(すなわち、“時間とともに変化する(Time Varring Value)”値)をファーストクラスの値として、それらを定義し、組み合わせ、そして関数の入力・出力に渡すことができる」という感じでしょうか。
とすると、ReactはFRPと言えません。なぜなら時間に伴なって変更される値(Time Varrying Value, Elmで言うSignal)に対する抽象操作ライブラリが整備されていないし、一次イベントを組み合わせて、新たなSignal(二次イベント)を構築する方法も提供されておらず、イディオムとしても確立されていないからです。
たとえば前述のマウスストーカーで、Elm版では★が前の★の位置においつこうとする動作を、Time.delayを用いて以下のように記述できました。
trace = Time.delay 100 -- 100ms遅延を与えたSignalを生成 p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う p2 = trace p1 -- 以降、一個前の座標を追うようにする
上ではtraceという一時関数を定義していますが、それを展開すると
p1 = Signal.sampleOn AnimationFrame.frame Mouse.position -- 最初はマウス座標を追う p2 = Time.delay 100 p1 -- 以降、一個前の座標を100ms遅延を与えて追う
になります。Signal(ElmにおけるファーストクラスのTime Varrying Value)であるマウス位置(Mouse.position)と、同様にSIgnalであるAnimation Frameのサンプリング(AnimationFrame.frame)を、合成操作「Signal.sampleOn」で組合せたり、「引数のSignalを500ms遅延して発火する操作Time.delay」で、Time varring valueを操作したものをTime varring valueとして生成しています。
かたや、Reactで同じことをするのは、
setPosition: function(x, y, refs) { this.setState({x:x, y:y}) if (this.props.followedBy != null) { setTimeout(()=>{ refs[this.props.followedBy].setPosition(x, y, refs) }, 100); }
の部分で、要は、位置の変更時(setState())に、setTimeoutで自分を追跡する★の位置を指定時間遅延させて発火させています。JSでの泥臭いイベントハンドラの定義と呼び出しの処理です。
最終的には、Elmは上記と同じ結果を生じさせるJSにコンパイルされます。FRPに魔法はありません。イベントハンドラとコールバックの組合せで実現しなければならなかったことを、抽象操作として利用できるので便利! 偉い! 見易い! わかりやすい!ということです。その抽象層がなければ定義上はFRPではない、と言えると思います。
とはいえ、一次イベントについては、renderから見たstateは透過的にアクセスでき、限定的にはFRPを実現しているとも言えます。先の例でいうmousemoveイベントで取得できるマウス位置は、刻々とかわるTime Varring Valueですが、renderの中では通常の値のようにあつかえているでしょう。二次イベントは、手作業でがんばってstateに登録することになります。
さらに、Reactを含むフレームワーク(もしくはそのアーキテクチャ)であるFluxは、Time Varring Valueの合成や、二次イベントのイディオム的にうまく実装できるのかもしれません。たとえば、あるStoreで、2つのイベントをまちあわせて発火する、みたいな感じ? Fluxはまだよく調べてないので、要調査です。またReactは意図的にUIライブラリに特化しているので、組合せて使えるFRPライブラリがあるのかもしれません。
まとめると、Reactは単体では定義上のFPRではありません。しかしFRPが提供する利点を一次イベントに関しては享受できます。
ReactはFPか?
私はYesだと思います。Reactの思想と実装はFP(関数型プログラミング(スタイル))に基いています。
Reactのコンポーネントのrenderは以下のような純粋関数です。
| 入力 | 出力 |
|:-|:-|
| this.props, this.state| React DOM, this.state |
おいおいまってくださいよ、propsはともかく、stateは変更するんだから純粋ではない、と思うかもしれません。しかし、setStateは、それは実際の変更ではなく、新しい値の設定なので、それを出力として返していると見ることができるのです。
stateを持つにせよもたないにせよ、renderは純粋関数として以下のように考えることができます。
|引数| |返り値 |
|:-|:-|:-|
| (props, state) | -> | (React DOM, state') |
ここでの、stateは、Reactコンポーネント1つのstateではなく、ReactDOM.render()全体で構築しようとするReact DOMツリー全体のstateを統合したものと考えてください(setStateがマージしていると考える)。
すると、おお、このstateとstate'を見ると、まさしくアレではないか。Stateモナド*2としてstateをもちまわり、renderとrenderが数珠繋ぎになって、貼り合さった全体のReact DOMが返却されるようにすれば、関数型としか言いようがありません。
つまり、Reactコンポーネントは、render以外を無視すれば、モナディックに合成され得る関数呼び出しを表現しています。render以外のメソッドは、これはただの関数であって、コンポーネントのクラスは関数置き場でしかありません。DOMのイベントハンドラに登録したり、他のコンポーネントのイベントをコールバックするのに用います。
(2015/11/08)上記はまちがっている。renderが純粋、いうのは正しいが、stateを変更するのはイベントハンドラの流れなのでrenderではもともとstateを更新も新規作成もしない。
FPとOOPの真の関係
そうだとしても、先のFRPの論法にしたがえば、FPとしての見た目、抽象があってこそのFPなのではないか? オブジェクト指向の部品を使って実現したものがFPと言えるのか、と思うかもしれません。
でも、FPというのは「値を変更しない」という制約にすぎません。「何かをしない」という制約は、FPを想定していない言語でも、BASICでも実現可能です。たとえば変数に再代入しないように注意したり、破壊的操作のないライブラリを使用すれば良いでしょう。だから、OOPでFPが実現できて何の不思議もない。
そしてそれは、varを使わずconstを使えといったミクロなレベルの話だけでなく、OOPの部品(クラス、オブジェクトインスタンス)を注意深く選択配置すれば、構造としてFPの思想をOOP上で実現でき、場合によってはFP言語による実現よりもわかりやすくなる場合すらあるのだ、と今では考えています。
なお、言わずもがなですが、Reactの利用者は、FPがどうのとか認識する必要はありません。単に使って、便利にあらわれてきている特徴を享受すれば良いです。
まとめ
- Reactは(単体では少なくとも)FRPではない。二次イベントのTime Varring Valueとしての構成や、Time Varring Valueの抽象操作を一般にサポートしていないため。ただし、一次イベントについては、renderから透過的にアクセスでき、FRPが提供する利点を得ることができる。
- ReactはFPである
- Reactはいいね
追記
こちらの記事によれば、
React が Reactive プログラミングを採用しなかった理由
React.js の開発者で Facebook につとめる Sebastian 氏は Staltz 氏の開発する Cycle.js を賞賛しながらも、Reactive プログラミングのスタイルを採用しなかった理由として、大多数の人が学ぶには大変であることを述べています (Hacker News)。
とのことです。
JCUnitのBuilder APIをためす
以前、以下の記事で紹介した、JCUnitの開発がすすんでいます。(開発ブログ, ソース)
主な拡張としては、状態機械をモデリングしてテスト生成、ビルダーAPIの整備などなど*1
Spockからの利用サンプルをかきなおしてみました。ビルダーAPIを使用しています。
@Grab('com.github.dakusui:jcunit:0.5.4') @Grab('org.spockframework:spock-core:1.0-groovy-2.4') import com.github.dakusui.jcunit.core.factor.Factors import com.github.dakusui.jcunit.core.factor.Factor import com.github.dakusui.jcunit.generators.TupleGenerator import com.github.dakusui.jcunit.core.FactorField; import com.github.dakusui.jcunit.core.tuples.Tuple; import com.github.dakusui.jcunit.constraint.constraintmanagers.ConstraintManagerBase; import com.github.dakusui.jcunit.constraint.ConstraintManager; import com.github.dakusui.jcunit.exceptions.UndefinedSymbol; import spock.lang.* class HelloSpec extends Specification { static ConstraintManager closureConstraintManager(names, Closure clos) { return new ConstraintManagerBase() { @Override boolean check(Tuple tuple) throws UndefinedSymbol { Binding binding = new Binding() names.each { if (!tuple.containsKey(it)) { throw new UndefinedSymbol(it) } binding.setProperty(it, tuple[it]) } clos.delegate = binding clos.call() } } } static Collection genPairwiseTestData(Map factors, Closure constraint = null) { def builder = new TupleGenerator.Builder() if (constraint != null) { builder = builder.setConstraintManager(closureConstraintManager(factors.keySet(), constraint)) } builder.setFactors(new Factors(factors.collect{k,v -> new Factor(k, v)})) .build() .collect{ it.values() }; } static intLevels = [ 1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE ]; static stringLevels = [ "Hello world", "こんにちは世界", "1234567890", "ABCDEFGHIJKLMKNOPQRSTUVWXYZ", "abcdefghijklmnopqrstuvwxyz", "`-=~!@#\$%^&*()_+[]\\{}|;':\",./<>?", " ", "" ] @Unroll def "分配法則(a=#a,b=#b,c=#c)"() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData([a:intLevels, b:intLevels, c:intLevels]) } @Unroll def test1() { expect: c*(a+b) == c*a + c*b where: [a,b,c] << genPairwiseTestData([a:intLevels, b:intLevels, c:intLevels], { return a > 0 && b != c }) } @Unroll def test2() { expect: (a+b).size() == a.size() + b.size() where: [a,b] << genPairwiseTestData([a:stringLevels, b:stringLevels]) } @Unroll def test3() { expect: a+b == b+a where: [a,b] << genPairwiseTestData([a:[1,2,3], b:intLevels]) } @Unroll def test4() { expect: a == b || a != b where: [a,b] << genPairwiseTestData([a:MyBoolean.values() as List, b:MyBoolean.values() as List]) } } enum MyBoolean { True, False }
以前のようにアノテーションを使用せずとも使用できるようになりました。
気付いた点としては、
- 各型のレベルのデフォルト値をAPIクラスからうまく参照できなかったので自前で定義しています。 (上記の static intLevels = [ 1, 0, -1, 100, -100, Integer.MAX_VALUE, Integer.MIN_VALUE ]の部分)
- SpockのデータパイプAPIにもうちょっと柔軟性が欲しい。今の呼び出し方だとデータ指向テストの疑似変数名の指定に重複があるので、疑似変数名を取得もしくは設定できるAPIがあると良いのだが。Spock Extensionで触れるフックがあるかな。
など。楽しいです。
*1:他に、生成テストデータの保存、再生などもなされているようです。
今こそッ、始めようGrailsブートキャンプ!!!!
以下のイベントが予定されています。 Grails3対応のGrailsブートキャンプです。
Grails3というのが出たタイミングで、ちょっと取り組み直しみよう、という向きに最適です。
山本さん(id:yamkazu)NTTSOFTが講師です。
ご興味があればぜひご検討を。
GroovyのクロージャとJava8のlambda式の違いについて
この両者は、似ているようでいて、基本的には別モノです。表にしてみます。
Groovyのクロージャ | java8のlambda式 | |
---|---|---|
導入時期 | 2003年 | 2014年03月 |
ローカル変数へのアクセス | 読み書き可能 | 実質的にfinal(変数そのものに対しては読み込みのみ) |
実装方法 | Closure |
MethodHandle, invokeDynamic.. |
型推論の根拠 | Closure<T>のTで返り値、@ClosureParamsで引数 | FunctionalInterface(SAM型) |
記法 | { 引数 -> 本体 } { 本体 } {-> 本体 } |
(引数) -> { 本体 } (引数) -> 式 () -> { 本体 } |
暗黙の動的なthis | delegateにより実現 | - |
性能 | ローカル変数をenclosingするため間接参照にするためのオーバーヘッドあり | 性能上のオーバーヘッド僅少 |
赤字がデメリット、青字が利点のイメージです。
このように、機能としては、Groovyクロージャの方が高機能だと言えます。特にローカル変数への書き込みアクセスが可能であることと、delegateで名前空間を制御できるのがGroovyクロージャの強力な利点です。
他方、lambda式の方が優れているのは、一つは性能です。コンパイラが頑張っているのか、静的型の面目躍如でfor文にも負けぬパフォーマンスを叩き出すという話を聞いたことがあります。もともと並列化が目的だったようなので、遅くっちゃしょうがないのです。
もう1点、lambda式の方が優れている面があるのは、型推論に関することであり、本エントリのテーマでもあります。lambda式の方が型推論の効果を得るのが簡単です。
型推論に関して
どういうことかというと、たとえばクロージャもしくはlambda式を引数にとるメソッドsomethingがあったとします。
Javaなら
void something(Function<String, String> x) { System.out.println(x.apply("abc")); }
こんな感じ、Groovyなら
void something(Closure<String> x) { println(x.apply("abc")) }
です。これを呼び出すとき、それぞれ
something { String it -> it.toUpperCase() } // Groovy
something( (String it) -> { it.toUpperCase() } ) // Java8
これは問題ありません。しかし、以下のように型を指定せず、型推論を期待したとき、どうなるでしょうか。
something { it -> it.toUpperCase() } // Groovy
something( (it) -> { it.toUpperCase() } ) // Java8
Java
Javaでは問題ありません。somethingに渡す引数の型は、Functional Interfaceから判るからです。上であれば、クロージャ引数の型はFunction<String, String> xなので、引数の型が最初のString、戻り値の型は2番目の型変数でこれもStringです。
somethingを呼びだす時に渡すラムダ式の引数には、文字列型の値が渡ってくることがコンパイラは知ることができるので、itの型を省略してもStringであることが推論されます。
Groovy
Groovyでも動的Groovyであれば、すなわち@CompileStaticや@TypeCheckedを指定しなければ、問題ありません。.toUpperCase()が呼び出されるオブジェクトが実行時点でStringであれば良いからです。
しかし、静的Groovy、すなわち@CompileStaticや@TypeCheckedを指定した場合、クロージャの引数の型を省略した
something { it -> it.toUpperCase() } // Groovy
は静的型チェックについてのコンパイルエラーになります。
[Static type checking] - Cannot find matching method java.lang.Object#toUpperCase(). Please check if the declared type is right and if the method exists.
@ line 11, column 24.
something( { it -> it.toUpperCase() } ) // Groovy
これはコンパイル時に、クロージャ「{ it -> it.toUpperCase() }」の引数itの型がわからないからです。
Closure<T>というクロージャ引数から、Tは返り値の型でわかりますが、クロージャに渡される引数の情報はなく、引数の型がわかりません。
これをコンパイルエラーにならなくするにはクロージャの引数型を明示指定するか、あるいはsomethingがわを以下のようにします。
@TypeChecked void something(@ClosureParams(value=SimpleType,options=['java.lang.String']) Closure<String> x) { System.out.println(x.call("abc")); }
@ClosureParamsアノテーションで、クロージャに渡す引数の型を明示します。ここでは単純にString型をクラス名FQCN文字列で指定しています。他に、渡した他の引数の型や、ジェネリックスの型などを指定することもできます。
なんでこうなった
GroovyのクロージャがもともとFunctional Interfaceを前提としていないことがあります。その理由は、クロージャ導入当時は型推論なんかないので、クロージャの引数の型を指定する方法は不要だったからです。
では今なぜFunctional Interfaceを導入しないのか?
一つは、Groovyでは、引数に渡すクロージャの引数の型や個数によって、動作をかえることができるからとのことです。たとえば、Map.each()には、{k,v ->}というクロージャを渡すとkey,valueが、{e ->}というクロージャを渡すとMap.Entryが引数としてわたってきます。これをPolymorphic Closureと呼ぶそうです。この2つのいずれか、という直和型をGroovy/Javaの型システムはうまく表現できないでしょう。またこの理由に加え、おそらく後方互換性的な理由などもあって選択されなかったのではないかと想像しています。
いずれにせよ、他にもdelegateの型推論の問題もあるので、仮にFunctional Interfaceを導入したとしても型推論の問題は全体としては解決しません。delegateの問題については一部それを解決しようとする@DelegatesToアノテーションに関するこちらの記事も参照ください。
相互運用
FunctionalInterfaceが求められる箇所でGroovyクロージャを使用すると、Closureのcall()を呼びだすようなlambda式が生成されてそれが渡されます。Groovy2.1だったかな?そこらでこれが可能となりました。実用上、これで良しとされています。Javaからlambda式をクロージャとして渡せるかというのはガリガリ書くことがたぶんできますが言語処理系のサポートはまったくありません。
まとめ
現在は、クロージャ引数を型推論させたいとき、つまり静的Groovyをしつつ型を省略したいときは、@ClosureParamsを使う必要があり、これはラムダ式における指定方法Functional Interfaceより煩雑ですが、ライブラリ側でしっかりとやれば、それを呼び出す側は簡潔かつ実行時型エラーフリー*1になります。GDKのクロージャをとるメソッド群ではしっかり指定がなされていますが、自分で定義する関数についても適用できます。
GroovyへのLambdaの導入
なお、過去のMLを見ると、GroovyでもJava8 lambdaを導入するってのはGroovy 3での開発予定(希望)項目ぐらいにはあがってたみたいです。しかし、もしやるとしても構文や意味、統合するのか併存か、互換性維持など、思いきりチャレンジングでしょうね。個人的には頭痛くなるので現状ぐらいでお腹いっぱいです。
渋谷JVMで「いまさら始めようGroovy」を話しました
昨日、渋谷JVM
にて発表させていただきました。
アテンドいただきましたコミュニティのみなさん、ビズリーチさま、聞いてくださったみなさん、ありがとうございました。また懇親会ごちそうさまでした(たこ焼たいへんおいしくいただきました!)。
他の言語の発表も聞くのも、またLTもたいへん楽しく参加させていただきました。名だたる皆さんと並んでの発表ということで、気おくれもし、準備とかパネルとか大丈夫かとか不安でしたが、サポートいただきなんとか乗り越えることができました。それとすばらしい会場ですねあそこはまた。起伏あり浜辺もあるし。
以下発表資料であります。
発表時に、会場のみなさんに「Groovy使っている人どのぐらいいますかー」と聞いたところ、非常に多くの割合*1で使われていたのでビックリし「およびでない、失礼しましたー」と帰りたくなりましたが、少しだけでも知見が増すところがあれば幸いなのですが、いかがなものでしたでしょうか。
かさねて御礼申しあげます。 以下、いただいたレスポンスなど
togetter.com shigemk2.hatenablog.com yukung.hatenablog.com takudo.github.io
*1:Gradle効果でしょうか。
TaPLのML実装をRustでやってみるシリーズ まとめ
型システム入門 −プログラミング言語と型の理論−、を読んでおります。
これをちまちまと読みつつ、せっかくなので各章にある「ML実装」の演習課題をRustにポーティングしていこうと思っています。Rustの勉強と型システムの勉強が一石二鳥かと思ったら二重苦というかなんというか、実に楽しいっすね。Rustは初めてC言語にさわったときレベルで楽しい。
できた分は以下にコード逐次公開していきます。