uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

List.collectManyで非決定計算

これはG*Advent callender 2016の記事です。 前日は@Ziphilさんの記事でした。明日はまた私@uehajの記事です。

出オチでタイトルオンリーです。

すごいHaksell楽しく学ぼう(以降H本)にも書かれているように、リストはモナドとみなすと「非決定計算」をあらわすものとみることができます。

つまりたとえばリスト[1,2,3]を「1か2か3のどれか」、リスト[2,3]を「2か3のどれか」をそれぞれあらわすもの、と見るってことです。その上で、[1,2,3] * [2,3] という操作を何等かの方法で表現すれば、「1か2か3のどれか」に「2か3のいずれか」を掛けたものという意味になり、結果として「1*2,2*2,3*2,1*3, 2*3,3*3]すなわち「1か4か6か3か6か9のどれか」を得ることができます。

さて、Groovyのリスト(厳密にはIterable)には、collectManyというメソッドがあって、これは上記のようにリストを非決定計算と考えたときの、任意の計算を「何等かの方法」で適用すること*1に相当します。

やってみます。

H本の例

ghci> [1,2] >>= \n -> ['a','b'] >>= \ch -> return (n,ch)  
[(1,'a'),(1,'b'),(2,'a'),(2,'b')]  

ここでは「1,2のどっちか」と「'a','b'のどっちか」をタプル化操作したもののどれか( [(1,'a'),(1,'b'),(2,'a'),(2,'b')] )を得ています。 これをGroovyで考えると以下のようになります。

groovy:000> [1,2].collectMany{n -> ['a','b'].collectMany{ ch -> [[n, ch]] }}
===> [[1, a], [1, b], [2, a], [2, b]]

Haksellと同様の結果が得られます(Groovyにはちゃんとしたタプルが無いのでリストで代用)。

ピタゴラス数を求める

次に同様にcollectManyを使ってピタゴラス数を求めてみましょう。

ピタゴラス数とは、a^2 + b^2 == cを満たす3つの数です。aが10までの範囲で求めてみます。

(1..10).collectMany{ a ->
(1..a).collectMany{ b ->
(a..a+b).collectMany{ c ->
    return (a**2 + b**2 == c**2) ? ["a=$a, b=$b, c=$c"] : []
}}}.each {
    println it
}

実行すると

a=4, b=3, c=5
a=8, b=6, c=10

を出力します。

覆面算を解く

もう一つ応用例です。以下のような有名な覆面算を解いてみます。

   SEND
+) MORE
~~~~~~~~~~
  MONEY

上記を成立させるように、アルファベットに重複せずに数を割り当てる問題です。

この問題は、Sが「1〜9のどれか」Eが「1〜9のどれか、かつSではない」…という可能性をもった非決定的な数同士であり、それらがある条件を満すかどうかを判定する非決定計算の問題とみることができます。 これをcollectManyを使って解くと以下のようになります。

def digits = 0..9 as List

def isAnswer(S,E,N,D,M,O,R,Y) {
  (S*1000+E*100+N*10+D) + (M*1000+O*100+R*10+E) == (M*10000+O*1000+N*100+E*10+Y)
}

(digits-0).collectMany { S ->
(digits-S-0).collectMany { M->
(digits-S-M).collectMany { E->
(digits-S-M-E).collectMany { O->
(digits-S-M-E-O).collectMany { N->
(digits-S-M-E-O-N).collectMany { R->
(digits-S-M-E-O-N-R).collectMany { D->
(digits-S-M-E-O-N-R-D).collectMany { Y->
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}}}}}}}}.each {
 println it
}

上記は以下を出力します。

  9567
+)1085
 10652

メタクラスさん登場

collectManyがちょっと字面上うるさいという場合、

List.metaClass.rightShiftUnsigned = { x->delegate.collectMany(x) }

のように演算子「>>>」に割り当てても良いかもしれません。

List.metaClass.rightShiftUnsigned = { x->delegate.collectMany(x) }

def digits = 0..9 as List

def isAnswer(S,E,N,D,M,O,R,Y) {
  (S*1000+E*100+N*10+D) + (M*1000+O*100+R*10+E) == (M*10000+O*1000+N*100+E*10+Y)
}

(
(digits-0) >>> { S ->
(digits-S-0) >>> { M->
(digits-S-M) >>> { E->
(digits-S-M-E) >>> { O->
(digits-S-M-E-O) >>> { N->
(digits-S-M-E-O-N) >>> { R->
(digits-S-M-E-O-N-R) >>> { D->
(digits-S-M-E-O-N-R-D) >>> { Y->
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}}}}}}}}).each {
 println it
}

現場からは以上です。

余談

あとは余談ですが、「}}}}}}}}.each {」の閉じ括弧の連続が嫌ですね。これはGroovyのクロージャの記法{->}の作りが良くなくて、今回の用法ではネストが必要になるので、複数の閉じ括弧が必要になるというわけです。ここをJava8のラムダ式の記法で書ければ良いと思いませんか? つまり

(
(digits-0) >>> (S) ->
(digits-S-0) >>> (M) ->
(digits-S-M) >>> (E) ->
(digits-S-M-E) >>> (O) ->
(digits-S-M-E-O) >>> (N) ->
(digits-S-M-E-O-N) >>> (R) ->
(digits-S-M-E-O-N-R) >>> (D) ->
(digits-S-M-E-O-N-R-D) >>> (Y) -> {
if (isAnswer(S,E,N,D,M,O,R,Y)) {
  return ["""\
  $S$E$N$D
+)$M$O$R$E
 $M$O$N$E$Y
"""]
  }
  return []
}).each {
 println it
}

と書く。やってやれないことはない、どうしたら良いのか?ということは明日の記事に回したいと思います(伏線)。

参考

uehaj.hatenablog.com

すごいHaskellたのしく学ぼう!
Miran Lipovača
オーム社
売り上げランキング: 60,257

*1:Haskellで言うモナディックな操作適用演算子>>=(bind)、Scalaで言うflatMap。

JWTトークン認証つきのWeb APIを作るのはGrails+Spring Security REST Pluginを使えば非常に簡単である件

先日のJGGUG WSでのLT資料を公開します。「JWT」は、ついジェーダブリュティーと読んでしまいましたが、正しい発音は「jot(ジョット)」だそうです。

ちなみに、上記デモで使用する認証スキームBearerというのがあるんですが、この由来は「持参人払い小切手(Bearer Check)」から来てるそうです*1。「持参人払い小切手」とは、自筆の署名(サイン)がしてある小切手を銀行窓口に持ってきたならば、その持ってきた人が誰であるかにかかわらず、現金を支払う、というものだとのことです。たしかにBearer認証スキームと同様ですね。

Web API: The Good Parts
Web API: The Good Parts
posted with amazlet at 16.05.19
水野 貴明
オライリージャパン
売り上げランキング: 38,557

*1:帰りに杉本さんに教えていただきました。ありがとうございました

Grails 3ってどうよ+ LT大会 - 日本Grails/Groovyユーザーグループ

連休明け5/13に、JGGUG(日本Grails/Groovyユーザ会)より、上記イベントがございます。

Sring BootをGSPとscaffold、GORMなどでさらに使いやすくしたフルスタックフレームワーク、Grails3について、実務適用している人たちとの情報提供、議論を行います。また恒例のLT大会も行います。

jggug.doorkeeper.jp

ふるってご参加ご検討ください。

Javaにも不変データ構造に基づいた(Cons セルベースの)リストがあるのだよ

TL;DR

JDK(tools.jar)中、com.sunパッケージ配下に、javacが内部的に使っている「com.sun.tools.javac.util.List 」が含まれており、これは不変データ構造としてのリストのように(Cons セルベースのリストのように)利用できる。

はじめに

ScalaのコードをGroovyやJavaなどに移植する際に、いつも悲しい思いをするのが不変データ構造に基づいたリスト処理ライブラリが見あたらない、ということでした。JDKの標準コレクションライブラリには、Scalascala.collection.immutable.Listのような、不変データ構造を用いて実装されているリストがありません。

ここで言う不変データ構造に基づいたリストとは、JDKjava.util.Collections.unmodifiableList()等で取得できるような変更禁止ビュー、つまり「要素追加などを禁止するバージョンのリスト」のことではなく、あるいはGrooovyの@Immutableによるイミュータブル指定でもなく、

「要素の追加は可能だが、その意味は、元のリストの変更ではなく、要素が追加された新しいリストを返す」

というもので、さらに実装として「リストを丸ごとコピーして追加して返す」のではなく、「追加前のリストと実体を共有することによって、メモリ利用効率を下げない(そして追加前のリストは不変であることも保証される)」ことが期待されます。もう少しぶっちゃけて言えば、Lispの「ConsセルベースのList」が欲しいのです*1

もちろん、サードパーティ製のものもあるにはあるでしょう。Functional javaのとかClojureのを使うとかはできるのかな。しかし基本的にはやみくもには外部依存を増やしたくはないものです。自分で作りたくもない。

com.sun.tools.javac.util.List

最近知ったのですが、少なくとも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さんの関わっていたプロジェクトではないですか。ある意味ご縁があるわけです。

使い方

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では以下の点で使いやすいです。

  • tools.jarは標準でGroovy実行時のパスに入っている
  • 名前のかぶるListではなく、「import com.sun.tools.javac.util.List as ConsList」のようにリネームしてインポートできる。

なお、残念ながらGroovyでは右結合の演算子オーバーロードできないので、ScalaHaskellの::のようにカッコなしで要素繋げる表記を使用することはできません(AST変換を使わないかぎり)。

もっと、もっと…欲しいんじゃっ…!

連想リストのマップっぽく使えるようした物が欲しいですが、それは見つけられませんでした(実装するのはたぶん難しくない)。

Purely Functional Data Structures
Chris Okasaki
Cambridge University Press
売り上げランキング: 50,281
Purely Functional Data Structures
Chris Okasaki
Cambridge University Press
売り上げランキング: 242,093

*1:LispのListが実際に不変であるか、といえば、実はそうではない。rplaca/rplacdといった破壊的操作があるから。以下で紹介するjavac.tools.Listも同じ。だとすると「不変データ構造と呼ぶべきか」が怪しいといういか間違い。本稿では「不変データ構造のように使えるリスト」として記載。(本件、@yasushia さんにもご指摘いただきました。ありがとうございます。)

Groovyをソースからコンパイルすると♪

groovyのソースをとってくるじゃろ。

% git clone https://github.com/apache/groovy.git

コンパイルするじゃろ。

% cd groovy
% ./gradlew installGroovy

すると、頭の中であの音楽が鳴り始める♪

http://f.st-hatena.com/images/fotolife/u/uehaj/20160305/20160305192747_original.png?1457173945

React Meets Grails 〜ReactはエンタープライズSPAの夢を見るか?〜

本記事は、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アプリを開発するときのGrailsの設定が知りたい人
  • サーバサイドJavaをずっとやってきて、モダンなJSの知識や経験があまりないけど、最近Reactってのが話題になっているのがさすがに気になるので挑戦したい人

背景

昨今では、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使用のために検討したところ、以下の問題に直面しました。

  • JSのライブラリでasset化されていないものについては自前で配置する必要がある。要はnpmエコシステムを活用できない(この時点で致命的?)。
  • asset化されているとしても最新版とは限らない。たとえば、react-assetpipeline pluginが現時点(2015/12/24)でサポートしているのはReact 0.13.2。
  • ES6トランスパイルの際のエラー発生箇所がわかりにくい(わからない?)。
  • 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関係の用語について

以降、いくつかの用語がでてきますので、JSに詳しくない方向けに簡単に解説しておきます。

用語 説明
Node.js JavaScriptインタプリタV8をコアとするJS処理系
JSX Reactで仮想DOMの構築に使用するJavaScriptに対する拡張記法
ホットリロード Reactの文脈では、個々のJSファイルが変更されたときに、状態(変数の値)を保持したまま、コード変更をアプリに反映し実行を継続すること。例えばモーダルダイアログを開いた状態で、JSコードのロジックを変更して保存すると、モーダルを表示したまま画面内容が変化する、といったことができる
ES2015 以前はES6と呼ばれていたEcmaScriptの標準仕様。本稿ではReactアプリの記述に全面的に使用

準備

本記事のサンプルコードを試すには、以下を事前にインストールしておくことが必要です。

  • Grails 2.5.3
  • Node.js(npmコマンドを使用)

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アプリ側の設定をそれぞれみていきます。

Grails

Grailsアプリ

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 = {}
}

context rootの変更

JSアプリの通信先として整合させるため、Webアプリとしてのcontext rootを以下のようにConfig.groovyに設定しておきます。この変更はGrails 3.x系であればデフォルトでそうなっているので不要です。

GRAILS_PROJ/grails-app/conf/Config.groovy

   :
grails.app.context = "/"

JSコンパイルの呼び出し

JSコードをコンパイルするように設定します。実際のコンパイルはreact-app配下のNodeモジュールとして実行します。LinuxMacOSに依存しないように*3、以下を含むpackages.jsonGRAILS_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-appgrails compile時に最新のweb-app/js/bundle.jsが生成されます。

また、1個1個のJSファイルが変更されたときの自動コンパイルはここでは走りません。走らせても良いのですが、この方式ではホットリロードに対応できないからです。webpack経由でホットリロード実現する方法については後述します。

web.xmlの設定

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アプリ側

基本的にごくごく普通のReactアプリです。

JSの明示的なコンパイル

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.jsGRAILS_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ソースを変更するとブラウザ画面が自動的に変更されるようになります。

CORS対応のためのリバースプロキシ

前述のホットリロード対応のため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にアクセスします。 行を選択するとデータの表示、編集が可能です。

つみのこし

以下のテーマは本記事では扱えませんでした。

  • 認証の問題
  • Scaffold for React(ドメインクラスの入力・編集フォームなどの自動生成)
  • サーバサイドレンダリング
  • FluxやReduxに代表されるアプリケーション全体の枠組み
  • テスト

まとめ

ということで、この数ヶ月ほそぼそとReactを学習してきた結果を紹介してみました。

冒頭に示した「React/SPAで開発することの実現性や可能性」についてですが、個人の感想としては、現時点では即座に業務アプリ開発に大規模に、というのは難しい、というものです。

鍵となるのは、テーブルやフォームなどの汎用ライブラリの存在で、今でも数自体は雨後の竹の子のようにぼこぼことあるのですが、選定に時間をとられすぎるし、やりたいことができないときに絶望にハマります。機能が十分で成熟していて、今後長く使えそうなものがないと業務摘要はむずかしいでしょう。かといってプリミティブなもので構築するとコード量が増えます。

とはいえ、それらが達成されるのも時間の問題という気もします。現時点でも、ページ遷移(ルーティング)のライブラリReact Routerなど、魅力的な機能が利用でき、もともと「SPAに代表される複雑なJSコードを含むアプリケーション」については、現時点でも少人数での開発での適用はアリで、保守性・可読性が遥かに高いコードが開発できます。一人プロジェクトないしコントロールが効く数人のプロジェクトなら状況が許せば是非つかいたいです。

将来的には、React Nativeなどもあわせ、広く普及していく可能性は高いと感じています。なにより楽しいです。ReactプログラミングはJSPなどに例える向きもあるようですが、個人的にはSwingなどのプログラミングにも近いと思います。

では、ハッピークリスマス!

*1:あるいはJSライブラリの入手方法と考えればWebjarというのもありますが、たとえばこれってコマンドとしてのwebpackとかも使えるんでしょうかね…。

*2:Routerはトップレベルである必要がある。そうしないとReact Routerを使う意味が薄れる。React Routerは神ライブラリである。

*3:セミコロンだからwindowsでも結局だめかもわからんね…。

(Babel 5における)ES6のモジュールを解説してみた

警告

以下でのモジュールの説明はトランスパイラである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においてモジュールとは何だったか

もともと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つがある。

    1. named export: 複数値をobjectで返す
    2. default export: 単一値を返す
    3. 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 "モジュール指定"
    • (1)複数値を{識別子1,識別子2..}で受ける。
    • (2)複数値を{識別子1 as 別名1,識別子2 as 別名2..}で受ける。
    • (3)複数値全体を単一のobjectで受ける(名前空間import)。
    • (1)import {a,b} from "module"
    • (2)import {a as X,b as Y} from "module"
    • (3)import * as X from "module"
    default exportが使用されたモジュール
    3 import 単一値を受けとる識別子, 複数値を受けとる指定 from "モジュール指定"
    • 2の(1)-(3)パターンに、単一値を受けとる識別子を追加したもの
    • (1)import A,{a,b} from "module"
    • (2)import A,{a as X,b as Y} from "module"
    • (3)import A,* as X from "module"
    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のモジュールは良く考えられてうまくできているよー。

    参考

    入門 React ―コンポーネントベースのWebフロントエンド開発
    Frankie Bagnardi Jonathan Beebe Richard Feldman Tom Hallett Simon HØjberg Karl Mikkelsen
    オライリージャパン
    売り上げランキング: 22,622
    WEB+DB PRESS Vol.87
    WEB+DB PRESS Vol.87
    posted with amazlet at 16.06.10
    佐藤 鉄平 小林 明大 石村 真吾 坂上 卓史 上原 誠 鳥居 英 佐藤 歩 泉水 翔吾 うさみ けんた 伊藤 直也 高橋 侑久 佐藤 太一 hayajo 橋本 翔 西尾 泰和 中島 聡 はまちや2
    技術評論社
    売り上げランキング: 196,459
    速習ECMAScript6: 次世代の標準JavaScriptを今すぐマスター!
    WINGSプロジェクト (2015-08-28)
    売り上げランキング: 1,140

    *1:なお、Node.jsでは、export defaultに相当する場合、実際に単一値を返すのが規約である(module.exportsに代入する)。Babelでは、このようなNode.jsのモジュールもES6のimport構文で扱えるように、__esModuleを含まない場合、{ default: 単一値 }にラッピングするようなコードにコンパイルされる。