連休明け5/13に、JGGUG(日本Grails/Groovyユーザ会)より、上記イベントがございます。
Sring BootをGSPとscaffold、GORMなどでさらに使いやすくしたフルスタックフレームワーク、Grails3について、実務適用している人たちとの情報提供、議論を行います。また恒例のLT大会も行います。
ふるってご参加ご検討ください。
連休明け5/13に、JGGUG(日本Grails/Groovyユーザ会)より、上記イベントがございます。
Sring BootをGSPとscaffold、GORMなどでさらに使いやすくしたフルスタックフレームワーク、Grails3について、実務適用している人たちとの情報提供、議論を行います。また恒例のLT大会も行います。
ふるってご参加ご検討ください。
JDK(tools.jar)中、com.sunパッケージ配下に、javacが内部的に使っている「com.sun.tools.javac.util.List 」が含まれており、これは不変データ構造としてのリストのように(Cons セルベースのリストのように)利用できる。
ScalaのコードをGroovyやJavaなどに移植する際に、いつも悲しい思いをするのが不変データ構造に基づいたリスト処理ライブラリが見あたらない、ということでした。JDKの標準コレクションライブラリには、Scalaのscala.collection.immutable.Listのような、不変データ構造を用いて実装されているリストがありません。
ここで言う不変データ構造に基づいたリストとは、JDKのjava.util.Collections.unmodifiableList()等で取得できるような変更禁止ビュー、つまり「要素追加などを禁止するバージョンのリスト」のことではなく、あるいはGrooovyの@Immutableによるイミュータブル指定でもなく、
「要素の追加は可能だが、その意味は、元のリストの変更ではなく、要素が追加された新しいリストを返す」
というもので、さらに実装として「リストを丸ごとコピーして追加して返す」のではなく、「追加前のリストと実体を共有することによって、メモリ利用効率を下げない(そして追加前のリストは不変であることも保証される)」ことが期待されます。もう少しぶっちゃけて言えば、Lispの「ConsセルベースのList」が欲しいのです*1。
もちろん、サードパーティ製のものもあるにはあるでしょう。Functional javaのとか、Clojureのを使うとかはできるのかな。しかし基本的にはやみくもには外部依存を増やしたくはないものです。自分で作りたくもない。
最近知ったのですが、少なくともjava8までのJDKには、com.sunパッケージ配下に「com.sun.tools.javac.util.List 」というクラスがあり、これがまさに求めるものです。ただし、com.sunパッケージにあることから判るように標準APIの一部ではなく、将来的に使える保証はありません。もし使う場合は、将来使えなくなるリスクを覚悟して使う必要があります。さらに、rt.jarではなくtools.jarに含まれているので、JREとしては使用できずJDKの元で使えるものです。javaコマンドから利用する場合、tools.jarをクラスパスに指定することが必要となるでしょう。
※ @kmizuさんにご指摘いただきましたが、これはコメントに記載があるようにGJCつまりGeneric Java Compiler由来のもので、確かこれはScalaのMartin Oderskyさんの関わっていたプロジェクトではないですか。ある意味ご縁があるわけです。
@uehaj なるほどー。知らなったのですが、"List is the main container class in GJC. Most data structures and algorthms in GJC use lists rather than arrays."
com.sun.tools.javac.util.Listの機能としてはこんな感じ。
意味(適当な表記) | 書き方 |
---|---|
nil | List.nil() |
car(a) | a.head |
cdr(a) | a.tail |
cons(a,b) | b.prepend(a) |
[a,b,c] | List.of(a,b,c) |
これを使えば、例えば2リストキューを用いた非破壊的キューもほらこのとおり(cf.20分でわかるPurely Functional Data Structures)。
import com.sun.tools.javac.util.List; import java.util.function.*; public class Queue<E> { public static class Tuple<E> { E value; Queue<E> queue; Tuple(E value, Queue<E> queue) { this.value = value; this.queue = queue; } } private List<E> front; private List<E> rear; public Queue(List<E> front, List<E> rear) { this.front = front; this.rear = rear; } public Queue() { this(List.nil(), List.nil()); } long size() { return front.size() + rear.size(); } public String toString(){ return "front<"+front+rear.reverse()+">rear"; } public Queue<E> add(E e) { return new Queue<E>(front, rear.prepend(e)); } public Tuple<E> remove() { if (front == List.nil()) { if (rear == List.nil()) { return new Tuple<E>(null, this); } return new Queue<E>(rear.reverse(), List.nil()).remove(); } else { return new Tuple<E>(front.head, new Queue<E>(front.tail, rear)); } } }
Groovyでは以下の点で使いやすいです。
なお、残念ながらGroovyでは右結合の演算子をオーバーロードできないので、ScalaやHaskellの::のようにカッコなしで要素繋げる表記を使用することはできません(AST変換を使わないかぎり)。
連想リストのマップっぽく使えるようした物が欲しいですが、それは見つけられませんでした(実装するのはたぶん難しくない)。
groovyのソースをとってくるじゃろ。
% git clone https://github.com/apache/groovy.git
コンパイルするじゃろ。
% cd groovy % ./gradlew installGroovy
すると、頭の中であの音楽が鳴り始める♪
本記事は、G*Advent Calendar 2015の24日目の記事です。 昨日は it__sseiさんの記事で、明日はtouchez_du_boisさんの記事です。
(2016/1/22追記) id:bati11_ さんが、本記事で作成しているボイラープレートコードのReact部分について、ステップバイステップの詳細かつ丁寧な以下の解説記事を執筆されております。是非併せてご参照ください。
bati11blog.hatenablog.com bati11blog.hatenablog.com bati11blog.hatenablog.com
本記事では、ReactによるSPAアプリをGrailsのフロントとして使用する方法を説明します。
以下が対象読者です。
昨今では、ReactとかES2015を筆頭とするJSの開発技術が成熟してきており、Javaで開発するWebアプリのフロント側などは業務システムを含め近未来にSPAが席巻するだろう、と思っています。従来は高かったSPAの開発コストが、Reactベースの高機能部品群とかUIビルダみたいなものの登場と成熟により低下していき、結果SPAの利点(使い勝手の良さ、セキュリティ向上、テストしやすさなど)が損益分岐コストを上まわってくるだろうからです。
しかし、その場合でもサーバサイドはRDBMSが捨てられて、AWS LambdaとかAPIゲイトウェイとかマイクロサービスに一挙に置き換わられることは(当面)なく、ドメインモデリングに基づいたGrailsが有効だろうと思っているのです。
この仮説、すなわち一般的な業務アプリのフロントをReact/SPAで開発することの実現性や可能性について感触を探ってみよう、というのがこの記事のもう1つの目的です。
herokuで起動している実行例はこちら。Github上のソースコードはこちらです。Webサイトではなく業務アプリっぽいものを想定したCRUDっぽいものです。
画面キャプチャも示します。
ちなみに完成度高くないのであしからず。
GrailsアプリでJavaScriptライブラリを使用するのは、asset pipelineを使うのが標準的です。しかし、React使用のために検討したところ、以下の問題に直面しました。
加えて、エラー発生時に、Web上の情報が豊富にあるかどうかも含めて考えると、asset pipelineで本格的にモダンJSの開発を行うのは今のところは難しそうです*1
なので、今回はnpmのエコシステムをそのまま使用するようにし、asset pipelineは使用しないようにします。GSPもSitemeshもいっさい使いません。GSPについては、本来ならReactを画面の一部に摘要することも可能なので理論的にはありうるのですが、ReactのルーティングライブラリであるReact Routerを使用するために断念しました*2。
上記方針を踏まえ、以下のように、Grailsアプリケーションのプロジェクトフォルダ(GRAILS_PROJ)の配下に、grails-appと並列するフォルダreact-appを作成し、そこにnode moduleとしてReactアプリを配置します。
ただし、実行開始点となるHTMLファイル(index.html)およびReactアプリのコンパイル結果であるJSファイル(bundle.js)は静的ファイルとしてGRAILS_PROJ/web-app/
配下に配置します。
GRAILS_PROJ ├── grails-app │ └── domain │ └── sample ├── scripts │ └── _Events.groovy ├── src │ └── templates │ └── war ├── web-app │ ├── index.html │ └── js │ ├── 89889688147bd7575d6327160d64e760.svg │ └── bundle.js └── react-app ├── node_modules ├── package.json ├── src │ ├── ajax.js │ ├── components/... │ └── index.js ├── server.js ├── hot.webpack.config.js └── webpack.config.js
以降、いくつかの用語がでてきますので、JSに詳しくない方向けに簡単に解説しておきます。
用語 | 説明 |
---|---|
Node.js | JavaScriptインタプリタV8をコアとするJS処理系 |
JSX | Reactで仮想DOMの構築に使用するJavaScriptに対する拡張記法 |
ホットリロード | Reactの文脈では、個々のJSファイルが変更されたときに、状態(変数の値)を保持したまま、コード変更をアプリに反映し実行を継続すること。例えばモーダルダイアログを開いた状態で、JSコードのロジックを変更して保存すると、モーダルを表示したまま画面内容が変化する、といったことができる |
ES2015 | 以前はES6と呼ばれていたEcmaScriptの標準仕様。本稿ではReactアプリの記述に全面的に使用 |
本記事のサンプルコードを試すには、以下を事前にインストールしておくことが必要です。
Node.jsのインストールには、私はndenvを使いました。
Reactアプリ側で、Reactライブラリ本体やwebpackその他必要なものは、npm install
コマンドで自動的にインストールされるので個別のインストールは不要です。
使用しているnpmライブラリやツールの主なものをNode.jsを含め簡単に紹介します。(参考:サンプルコードのpackage.json)
ツール | 説明 |
---|---|
webpack | Node.jsでブラウザ上で動作するJSコードを開発する際に使用する、大艦主義的とも呼ばれる包括的ツール。トランスパイル、ビルドツール、CSSコンパイルなどの機能を含む |
npm | Node package manager。Node.jsに含まれるパッケージ管理ツール |
ESLint | JSの静的解析ツール。ES2015に対応する |
Babel | 以前は6to5と呼ばれていたトランスパイラ。JSXも標準でサポート。本稿ではwebpackのローダの一つとして使用 |
以降、Grails側、Reactアプリ側の設定をそれぞれみていきます。
Web APIを実装するGrailsアプリを開発します。ここでは簡単に、ドメインクラスBook
を定義し、Restリソースとして公開するために@Resource
アノテーションを指定します。
GRAILS_PROJ/grails-app/domain/sample/Book.groovy
package sample import grails.rest.Resource @Resource(uri="/api/books") class Book { String title Integer price static constraints = {} }
JSアプリの通信先として整合させるため、Webアプリとしてのcontext rootを以下のようにConfig.groovyに設定しておきます。この変更はGrails 3.x系であればデフォルトでそうなっているので不要です。
GRAILS_PROJ/grails-app/conf/Config.groovy
:
grails.app.context = "/"
JSコードをコンパイルするように設定します。実際のコンパイルはreact-app配下のNodeモジュールとして実行します。LinuxやMacOSに依存しないように*3、以下を含むpackages.jsonをGRAILS_PROJ直下に用意し、
{ "scripts": { "start": "cd react-app; npm start", "deploy": "cd react-app; npm run-script deploy", "lint": "cd react-app; npm run-script lint" } }
GRAILS_PROJ/scripts/_Events.groovyで以下のように実行します。
eventCompileStart = {kind -> : def proc = "npm run-script deploy".execute() :
JSコンパイル自体は、npmで実行されます(後述)。
これでgrails run-app
やgrails compile
時に最新のweb-app/js/bundle.js
が生成されます。
また、1個1個のJSファイルが変更されたときの自動コンパイルはここでは走りません。走らせても良いのですが、この方式ではホットリロードに対応できないからです。webpack経由でホットリロード実現する方法については後述します。
React Routerのbrowser historyを使用するために、web.xmlを変更します。grails install-template
を実行し、生成されるsrc/templates/war/web.xml
に以下を追加します。
GRAILS_PROJ/src/templates/war/web.xml
<error-page> <error-code>404</error-code> <location>/index.html</location> </error-page>
また、context rootがアクセスされたときにindex.htmlが表示されるように設定します。
GRAILS_PROJ/src/templates/war/web.xml
<welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list>
index.htmlに以下の設定を行います。
GRAILS_PROJ/web-app/index.html
<html> <head> : <meta charset="UTF-8"> </head>
基本的にごくごく普通のReactアプリです。
webpackの設定で、コンパイルしたJSファイルをGRAILS_PROJ/web-app/js
配下に出力するようにします。
GRAILS_PROJ/react-app/webpack.config.js
: output: { path: path.join(__dirname, '../web-app/js'),
これはGrailsのcompile event時に呼び出されますし、明示的にnpm run-script deploy
を実行しても良いです。
Reactのホットリロード(JSファイル更新検出に伴う自動コンパイル、自動リロード)に対応する機構はGrails本体では今のところ実現できないので(WebSocketで制御する必要がある)、webpackが提供するwebpack-dev-serverを使用します。詳しくは説明しませんが、設定しているのは、GRAILS_PROJ/react-app/server.js
とGRAILS_PROJ/react-app/hot.webpack.config.js
です。
この設定では、localhostの3000番ポートからindex.htmlとJSアプリ(bundle.jsファイル)をダウンロードすることになります。
webpack-dev-serverを起動するには以下のようにします。
$ cd GRAILS_PROJ/react-app $ npm start
以降はlocalhost:3000をアクセスすることで、JSソースを変更するとブラウザ画面が自動的に変更されるようになります。
前述のホットリロード対応のためindex.htmlとJSの閲覧ポートを3000に変更すると、Web APIの通信先であるGrailsアプリのデフォルトポート番号8080と異なってしまい、CORS(Cross-Origin Resourc Sharing)のブラウザ制約にひっかかってしまいます。
ここでうかつにAPIサーバ側のCORSヘッダ設定を緩めてしまうのはセキュリティ上問題なので、webpack-dev-serverが提供するリバースプロキシの機能を使用し、"/api"で始まるURIへのリクエストをGrailsアプリが実行されている8080ポートに転送するようにします。
GRAILS_PROJ/react-app/server.js
proxy: { '/api/*': "http://localhost:8080", // Proxy to running Grails application. },
まずcd react-app
してnpm install
、そしてGRAILS_PROJに戻ってgrails run-app
し、別窓でnpm start
してhttp://localhost:3000にアクセスします。
行を選択するとデータの表示、編集が可能です。
以下のテーマは本記事では扱えませんでした。
ということで、この数ヶ月ほそぼそとReactを学習してきた結果を紹介してみました。
冒頭に示した「React/SPAで開発することの実現性や可能性」についてですが、個人の感想としては、現時点では即座に業務アプリ開発に大規模に、というのは難しい、というものです。
鍵となるのは、テーブルやフォームなどの汎用ライブラリの存在で、今でも数自体は雨後の竹の子のようにぼこぼことあるのですが、選定に時間をとられすぎるし、やりたいことができないときに絶望にハマります。機能が十分で成熟していて、今後長く使えそうなものがないと業務摘要はむずかしいでしょう。かといってプリミティブなもので構築するとコード量が増えます。
とはいえ、それらが達成されるのも時間の問題という気もします。現時点でも、ページ遷移(ルーティング)のライブラリReact Routerなど、魅力的な機能が利用でき、もともと「SPAに代表される複雑なJSコードを含むアプリケーション」については、現時点でも少人数での開発での適用はアリで、保守性・可読性が遥かに高いコードが開発できます。一人プロジェクトないしコントロールが効く数人のプロジェクトなら状況が許せば是非つかいたいです。
将来的には、React Nativeなどもあわせ、広く普及していく可能性は高いと感じています。なにより楽しいです。ReactプログラミングはJSPなどに例える向きもあるようですが、個人的にはSwingなどのプログラミングにも近いと思います。
では、ハッピークリスマス!
以下でのモジュールの説明はトランスパイラであるBabel 5,6で動作を確認した振舞いについての記述です。2015年11月現時点で、ECMAScriptのモジュール仕様策定範囲は、本来の全体範囲のまだ一部であるとのことです。その状況でのBabelの実装は、良く言えば先行的、悪く言えば将来そのままである保証はなく、現時点でも他のES2015をサポートする処理系との間での相互運用の保証はありません。また、現時点でBabelのモジュール機能を使うこと自体にリスクがあるという意見もあります。CommonJS側からBabelが生成したモジュールをCommon JSモジュールとして読み込もうとしたときの互換の問題として、Babel5で可能だったことがBabel6では利用不可になる、といったことも起きているようです。
そこらへんを含めて解説されているこちらの資料が参考になります。
(2015/11/18追記)
ES2015(以降、ES2015はES6と書く。長いので)のモジュール機能を説明する。
ES6のモジュールはNode.JSとかCommonJSのそれと近いので、それらの使用経験があれば類推できる部分が大きい。しかし、「Node.JSのモジュールの使用経験がない人」にとってはつらいかもしれない。なので、前提知識をなるべく必要としない形でES6モジュールの用法についてまとめてみる。
もともとJSに言語仕様としてのモジュール(Javaで言うpackage/import、rubyで言うrequireなどで実現される機能)はなかった。では、従来のJSでモジュール管理はどういうものだったかというと、.jsという拡張子を持ったテキストファイルから文字列を読み込んでevalっぽいもので評価して中でexportされているオブジェクトを識別子にバインドするような処理で実現されていた。ちなみに、インポートされるシンボルで使用されるピリオド(.)は、名前空間の区切りとかではなく、objectのプロパティを実行時にたどっている。
前述した、モジュール機構を実装する既存のライブラリの呼び出しに対するシンタックスシュガー的なものを、ECMAScript言語仕様できっちり規定して、将来に渡っても実装に左右されないようにした。内部機構を隠蔽することにもなっており、非同期読み込みなどの効率化を可能としたり、静的解析や事前コンパイルなど最適化を可能としたり、型チェックなどにも寄与できるなど利点が大きい。ES6のモジュールは、ES6の価値の主要なものの一つである。とはいえ、現状では、ブラウザネイティブ始め、まだ実装されていない処理系も多く、トランスパイラ実装では全ての利点が享受できるわけではないが、とりあえずトランスパイラBabelだけで使えれば良く、Babelを使い続ける限り将来互換もなんとかなるやと楽観的な人は使おう。なお、React界隈ではサンプル含めBabel前提でモジュールが良く使われている印象を持っている。(2015/11/18追記)。
ES6モジュールを実現する構文は、exportとimportの2つの宣言である。
これらは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)の段階でのモジュールからの値の返し方には、以下の3つがある。
以降、値がどう返されるかをそれぞれ見ていく。
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 }
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追記)、後勝ちで上書きされるようである。
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文のシンタックスシュガーで隠蔽される。
export {x} from "mod"; export {v as x} from "mod"; export * from "mod";
説明は略。
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" | 副作用を期待するモジュール |
from句でモジュールを指定する。呼び出す側のJSファイルと同じ、もしくはサブディレクトリや上位ディレクトリに配置されているJSファイルで定義されているモジュールを読み込むには、「./」や「../」で始まるファイル名の相対指定で指定すればよい(拡張子不要)。なおNode.jsではnode_modules配下のモジュールは「./」では始めなくてよい。この仕組みはNodeの仕様だと思うが、ES6でも定義された仕様なのかは知らない。
結局、JSのモジュールの使用は、named系とdefault系の独立した2系列があり、以下の区別である。
図にまとめるとこうである。
ちなみに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補足追記)
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
default export、すなわち単一値を返すモジュールはimport x from ..のように指定するとその単一値がxに保持される。
(例2-1)
// module4.js export default 1
import x from "./module4" console.assert(x==1)
module4の返す値がxに代入される。xは任意につけてよい名前である。
単一値と複数値の両方を返すモジュールからの値を以下のように受けとることができる。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 exportの指定のみしかない単一値のみを返すモジュールを、named importすると、例えば {foo, bar} fromでimportすると、foo, barはundefinedとなる。
named exportの指定のみしかない、複数値を返すモジュールを、default importすると、例えば X from ..でimportすると、Xはundefinedになる。
ここらへんや、次の項目の振舞いは、「単一値のexportは複数値の"default"プロパティで実現されている」ことの帰結である。
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のモジュールは良く考えられてうまくできているよー。
Reactは、Facebookが開発した、JSのUIフームワークもしくはライブラリです。Reactが提供する中核機能は以下です。
効用は、再利用性と保守性・可読性向上です。特に、Reactで作成した画面部品のコンポーザビリティが高く、細粒度のUI部品利用の発展充実が期待されます。作りは通常のJSクラスライブラリであり、覚えたりすることは多くありません。
Reactが解決しようとする問題は、大規模化するSPAにおいて、大域状態をDOM内の値として管理し、無数のイベントハンドラがただあるだけ、といった「設計不在」への対処です。Reactは「UIはこう設計しようぜ」という導きであり、設計を大事だと思う一部の人には大受けします。やんややんや。
さて、以前、以下の記事では、関数型リアクティブプログラミング(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版では、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の方が私にはわかりやすく感じました。
まず、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が提供する利点を一次イベントに関しては享受できます。
私は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を更新も新規作成もしない。
そうだとしても、先のFRPの論法にしたがえば、FPとしての見た目、抽象があってこそのFPなのではないか? オブジェクト指向の部品を使って実現したものがFPと言えるのか、と思うかもしれません。
でも、FPというのは「値を変更しない」という制約にすぎません。「何かをしない」という制約は、FPを想定していない言語でも、BASICでも実現可能です。たとえば変数に再代入しないように注意したり、破壊的操作のないライブラリを使用すれば良いでしょう。だから、OOPでFPが実現できて何の不思議もない。
そしてそれは、varを使わずconstを使えといったミクロなレベルの話だけでなく、OOPの部品(クラス、オブジェクトインスタンス)を注意深く選択配置すれば、構造としてFPの思想をOOP上で実現でき、場合によってはFP言語による実現よりもわかりやすくなる場合すらあるのだ、と今では考えています。
なお、言わずもがなですが、Reactの利用者は、FPがどうのとか認識する必要はありません。単に使って、便利にあらわれてきている特徴を享受すれば良いです。
こちらの記事によれば、
React が Reactive プログラミングを採用しなかった理由
React.js の開発者で Facebook につとめる Sebastian 氏は Staltz 氏の開発する Cycle.js を賞賛しながらも、Reactive プログラミングのスタイルを採用しなかった理由として、大多数の人が学ぶには大変であることを述べています (Hacker News)。
とのことです。
以前、以下の記事で紹介した、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 }
以前のようにアノテーションを使用せずとも使用できるようになりました。
気付いた点としては、
など。楽しいです。
*1:他に、生成テストデータの保存、再生などもなされているようです。