uehaj's blog

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

Dartの変数定義時の修飾static/final/const、そしてconst constructorについて

Dartの変数定義時の修飾static/final/const、そしてconst constructorについて」という記事をQiitaに書きました。

qiita.com

結論としては、「コンパイル時レイトレ」とかはできません(たぶん)。

Grails&React CRUDビュー生成2017 〜Reactベースの管理画面構築ライブラリAdmin-On-Restの紹介〜

これはG*Advent callender 2017の24日の記事です。 昨日23日の記事は mshimomuさん、明日25日の記事は未定です。

完全に一年ぶりの記事です。みなさん、いかがお過しでしたでしょうか。今年Qiitaとかに書いた記事を別にまとめましたが、わたしはReactを良く書いてた一年でした。

本記事のテーマは以下の2つです。

  • GrailsのWeb APIサーバのCRUDビューをいかに簡単に作るか
  • RESTfulサーバ/任意のサーバに対する強力なダッシュボード・管理コンソール開発用ReactコンポーネントライブラリであるAdmin-On-Restの紹介、使い方

もくじ

去年までのあらすじと今年の方針

一昨年のG*Advent Calendar昨年のG*Advent Calendarでは、バックエンドを「GrailsのRESTfulなWeb APIサーバ」それをCRUDするWebアプリケーションフロントエンドを「React SPA(redux,react-router,..)」という組合せで開発し、ボイラープレート(雛形)として実装しました。

さらに去年は、GrailsドメインクラスからJSON Schemaを自動生成し、テーブルや入力フォームを生成するという工夫をしたのでした。 さて、今年はAdmin-On-RestというReactベースの素敵なライブラリがありますのでこれを使ってみます。

Admin-On-Restとは何か

Admin-On-Restは、主にRESTfulサーバをバックエンド(データ供給源)として、そのデータをテーブル形式のCRUD編集や ページネーション表示するようなWebアプリケーションを構築するためのライブラリです。用途としては管理画面を作ることを想定しているようです。RailsでいうRailsAdminみたいなものですね。

Admin-On-Restはreact-router, redux, redux-form, redux-saga, material-ui, i18n,認証などのReactでの定番の機構を用いて疎結合かつ汎用的に作られており、管理画面の構築のみならず、アプリの基本構造として使用できますし、他のアプリに部品として組込むこともできます。 過去にAngularJS用の高機能な管理画面ライブラリとして有名だったng-adminの開発チームがReactに移行して作っているようです。

Admin-On-Restのデモ動画

github.com http://static.marmelab.com/admin-on-rest.gif (https://marmelab.com/admin-on-rest/Tutorial.html より引用)

admin-on-rest demo from Francois Zaninotto on Vimeo.

Admin-On-Restの特記すべき点

Admin-On-Restの特徴は、高機能さと、カスタマイズ性の両立です。react-router, redux, redux-form, redux-sagaなどを活用することによって、Reactというコンポーネント指向ライブラリの利点を如何なく発揮しています。「簡単に使いたければ簡単に」「実務的に必要があれば徹底的にカスタマイズする」ことができ、かつ保守性可読性を規模にスケールさせて維持し続けることができます。これはページ遷移ベースのサーバサイド画面生成技術では、人間の知力を前提にすると現実的には不可能だったことです。Reactが切り開く地平(の序の口)とも見ることができるでしょう。

REST Clientによるデータソースの抽象化

Admin-On-RestはもちろんGrailsに依存するものではありません。バックエンドは任意のRESTful Web APIサーバにも留まらず、 「データを供給する何か」として抽象化されて扱われます。以下はadmin-on-restのページにある説明図ですが、

https://github.com/marmelab/admin-on-rest/raw/master/docs/img/rest-client.png (https://github.com/marmelab/admin-on-rest/blob/master/docs/index.md より引用)

上でいう「REST Client」は実はデータ集合を扱う基本操作を備えた汎用的なデータプロバイダー/コネクタであり、 コネクタには標準で添付されるもの以外に以下があります。

GraphQLは後述します。その他のフレームワークやサービス(やRDBMS)に対するものは、いずれも大きく言えばRESTfulなWebAPIに対するものなわけで、 なぜそれぞれにバリエーションが必要かといえば、特にページネーションを実現するための部分取得や、データ集合全体でどのページ範囲を見ているかを表現する流儀が異なるからです。

Admin-On-RestをGrailsに繋ごう

Grailsに繋ぐためには、GrailsRest API自動生成で作られるAPIに対応したコネクタ(REST Client)を探すか設定するか作るか、あるいはGrails側で生成されるAPIの実装コードをカスタマイズして、上のいずれかと互換のある動作をするように変更する必要があります。今回は、Grails用のコネクタを作成します。

ちゃっちゃっと行きます。成果物のソースはこちら

プロジェクト作成

Grailsのreactプロファイルを使用してプロジェクトを初期作成します。

# sdk use grails  # SDKMAN使用

Using grails version 3.3.2 in this shell.

# grails create-app --profile react grailsReactAdminOnRest
# cd grailsReactAdminOnRest

reactプロファイルはGradleのマルチプロジェクト構成になっており、serverディレクトリ配下にgrailsアプリが、clientディレクトリ配下にcreate-react-appを使用したReactアプリが作成されます。

% tree -L 2 .  
├── README.md
├── client
│   ├── README.md
│   ├── build.gradle
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── public
│   ├── src
│   └── yarn.lock
├── gradle
│   └── wrapper
├── gradlew
├── gradlew.bat
├── server
│   ├── build
│   ├── build.gradle
│   ├── gradle.properties
│   ├── grails-app
│   ├── grails-wrapper.jar
│   ├── grailsw
│   └── grailsw.bat
└── settings.gradle

ドメインクラス作成

BookとAuthorの2つのドメインクラスを作成します。BookはAuthorに所属するn:1の関連を持たせます。

// server/grails-app/domain/sample/Book.groovy
package sample
import grails.rest.*

@Resource(uri='/api/book')
class Book {
  String title
  Integer price

  static belongsTo = [author: Author]
}
// server/grails-app/domain/sample/Author.groovy
package sample
import grails.rest.*

@Resource(uri='/api/author')
class Author {
  String name

  static hasMany = [books: Book]
}

APIサーバの動作確認

# grails run-app

別端末から確認

% curl -X GET http://localhost:8080/author
[]

% curl -X POST --header 'Content-Type: application/json' http://localhost:8080/author.json -d '{"name":"uehaj"}'
{"id":1,"name":"uehaj"}

% curl -X POST --header 'Content-Type: application/json' http://localhost:8080/author.json -d '{"name":"yamada tarou"}'
{"id":2,"name":"yamada tarou"}

% curl -X GET http://localhost:8080/author
[{"id":1,"books":[],"name":"uehaj"},{"id":2,"books":[],"name":"yamada tarou"}]

% curl -X POST --header 'Content-Type: application/json' http://localhost:8080/book -d '{"title":"Groovy Programming", "price":1800}'
{"message":"[class sample.Book]クラスのプロパティ[author]にnullは許可されません。","path":"/book/index","_links":{"self":{"href":"http://localhost:8080/book/index"}}}

% curl -X POST --header 'Content-Type: application/json' http://localhost:8080/book -d '{"title":"Groovy Programming", "price":1800, "author": 1}'
{"id":1,"title":"Groovy Programming","price":1800,"author":{"id":1}}

% curl -X GET http://localhost:8080/book/1
{"id":1,"title":"Groovy Programming","price":1800,"author":{"id":1}}

サーバ側の端末に戻ってGrails APIサーバをいったん落しておきます。

^C

以降は、すべてクライアント側の設定やコード追加になります。

クライアントの初期化

clientディレクトリ配下にはpackage.jsonを含むNPMのプロジェクトが作成されています。(create-react-appベース)

不要なNPMモジュールをuninstall、不要なソース削除します。。

# cd ../client
# npm uninstall react-bootstrap
# rm -r src/App* src/css src/images

admin-on-restをNPMモジュールとしてインストールします。

# npm install -S admin-on-rest

Reactアプリとしての入り口を定義

index.jsを以下のように定義します。create-react-appが生成するindex.jsから不要なCSSのimportを削除しただけです。

// client/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

上でimportされているApp.jsは以下のように定義します。 AdminコンポーネントはAdmin-On-Restで構成するアプリのトップレベルのコンポーネントです。

// client/src/App.js
import React from 'react';
import { simpleRestClient, Admin, Resource } from 'admin-on-rest';

import * as Config from './config';

export default props => (
  <Admin simpleRestClient={simpleRestClient(Config.SERVER_URL)} />
);

サーバとクライアントの両起動

これで一応クライアントも起動するはずなので、サーバとクライアントの両方起動してみます。

# cd ..
#  ./gradlew bootRun -parallel

ブラウザのhttp://localhost:3000/が開き以下の画面が表示されると思います。 リソースが定義されていないので表示内容は空です。

f:id:uehaj:20171223230925p:plain

Grails用REST Client の作成

simple.jsを元にして以下の細工をします。

  • ページネーションの修正
    • ページネーションの開始位置、1ページあたりのデータ項目数の指定をGrailsにあわせる(range=[page,perPage]→max=,offset=)
    • デフォルトのAdmin-on-RestではRangeヘッダの値から全体件数を表示するページネーションのUIを生成するが、Grailsが@Resorucesで生成するデフォルトWeb APIは返さないので全体件数はあきらめる。Content-Rangeを受けとる処理を削除
  • ソート順序の指定をGrailsに合せる(sort=[field,order]→sort=, order=)
  • filterの処理はとりあえず今回は無視する

作成したgrallsRestClient.jsのソースはこちらにあります。

Admin-On-Restのカスタマイズ

さて上記の準備ができたならば、あとはAdmin-On-Restで画面を作っていくという話になります。(Grailsとは独立)。 基本的に、リソースを追加していくとそれがページになります。

表示リソースの追加(Author)

まず、Authorリソースを作成編集できるように修正していきます。 Authorリソースの編集のための画面コンポーネント群を定義するsrc/resources/author.jsを以下の内容で作成します。

// client/src/resources/author.js
import React from 'react';
import {
  List,
  Datagrid,
  Edit,
  Create,
  SimpleForm,
  TextField,
  EditButton,
  DisabledInput,
  TextInput,
} from 'admin-on-rest';

import ErrorBoundary from '../ErrorBoundary';
import Pagination from '../Pagination';
import AuthorIcon from 'material-ui/svg-icons/social/person';
export { AuthorIcon };

export const AuthorList = props => (
  <ErrorBoundary>
    <List {...props} pagination={<Pagination />}>
      <Datagrid>
        <TextField source="id" />
        <TextField source="name" />
        <EditButton basePath="/author" />
      </Datagrid>
    </List>
  </ErrorBoundary>
);

const AuthorTitle = ({ record }) => {
  return <span>Author {record ? `"${record.title}"` : ''}</span>;
};

export const AuthorEdit = props => (
  <ErrorBoundary>
    <Edit title={<AuthorTitle />} {...props}>
      <SimpleForm save={() => console.log('save')}>
        <DisabledInput source="id" />
        <TextInput source="name" />
      </SimpleForm>
    </Edit>
  </ErrorBoundary>
);

export const AuthorCreate = props => (
  <ErrorBoundary>
    <Create title="Create a Author" {...props}>
      <SimpleForm>
        <TextInput source="name" />
      </SimpleForm>
    </Create>
  </ErrorBoundary>
);

ここでExportしているコンポーネント群(AuthorList, AuthorEdit, AuthorCreate, AuthorIcon)を使い、App.jsのAdminコンポーネントの子要素としてResourceコンポーネントを配置します。

// client/src/App.js
import React from 'react';
import { simpleRestClient, Admin, Resource } from 'admin-on-rest';
import {
  AuthorList,
  AuthorEdit,
  AuthorCreate,
  AuthorIcon,
} from './resources/authors';

import * as Config from './config';

export default props => (
  <Admin simpleRestClient={simpleRestClient(Config.SERVER_URL)} >
    <Resource
      name="author"
      list={AuthorList}
      edit={AuthorEdit}
      create={AuthorCreate}
      icon={AuthorIcon}
    />
   </Admin>
);

これでクライアントで以下のようにAuthorを追加、編集できるようになります。 AuthorからBookへの依存がないので、Bookを作成しなくても作成できます。

f:id:uehaj:20171223222914p:plain f:id:uehaj:20171223222953p:plain

表示リソースの追加(Book)

次に、Bookリソースの編集機能を追加します。 画面コンポーネント群を定義するsrc/resources/book.jsを以下の内容で作成します。

Bookは外部キーでAuthorを参照しているので、画面上での参照・編集できるようにコンポーネントを使用します。

// cliet/src/resources/book.js
import React from 'react';
import {
  List,
  Datagrid,
  Edit,
  Create,
  SimpleForm,
  TextField,
  EditButton,
  DisabledInput,
  TextInput,
  NumberInput,
  ReferenceInput,
  ReferenceField,
  SelectInput,
} from 'admin-on-rest';

import ErrorBoundary from '../ErrorBoundary';
import Pagination from '../Pagination';
import BookIcon from 'material-ui/svg-icons/action/book';
export { BookIcon };

export const BookList = props => (
  <ErrorBoundary>
    <List {...props} pagination={<Pagination />}>
      <Datagrid>
        <TextField source="id" />
        <TextField source="title" />
        <TextField source="price" />
        <ReferenceField label="Author" source="author.id" reference="author">
          <TextField source="name" />
        </ReferenceField>
        <EditButton basePath="/book" />
      </Datagrid>
    </List>
  </ErrorBoundary>
);

const BookTitle = ({ record }) => {
  return <span>Book {record ? `"${record.title}"` : ''}</span>;
};

export const BookEdit = props => (
  <ErrorBoundary>
    <Edit title={<BookTitle />} {...props}>
      <SimpleForm>
        <DisabledInput source="id" />
        <TextInput source="title" />
        <NumberInput source="price" />
        <ReferenceInput label="Author" source="author.id" reference="author">
          <SelectInput optionText="name" />
        </ReferenceInput>
      </SimpleForm>
    </Edit>
  </ErrorBoundary>
);

export const BookCreate = props => (
  <ErrorBoundary>
    <Create title="Create a Book" {...props}>
      <SimpleForm>
        <TextInput source="title" />
        <NumberInput source="price" />
        <ReferenceInput
          label="Author"
          source="author.id"
          reference="author"
          allowEmpty>
          <SelectInput optionText="name" />
        </ReferenceInput>
      </SimpleForm>
    </Create>
  </ErrorBoundary>
);

外部キー参照するフィールドについて、BookListではReferenceFieldを、BookEdit, BookCreateではReferenceInputを使用します。BookCreateではReferenceInputコンポーネントにallowEmpty属性を指定する必要があります。

Grailsが出力するBookのJSONレコードには、authorに対する外部キーが、

{"id":1,"title":"Groovy Programming","price":1800,"author":{"id":2}}

のように含まれるため、ReferenceField、ReferenceInputのsource属性に"author.id"を指定しています。

Bookリソースを増やしたのでApp.jsのAdmin配下にResourceコンポーネントを追加します。

// client/src/App.js
:
export default props => (
  <Admin restClient={restClient(Config.SERVER_URL)}>
    <Resource
      name="book"
      list={BookList}
      edit={BookEdit}
      create={BookCreate}
      icon={BookIcon}
    />
    <Resource
      name="author"
      list={AuthorList}
      edit={AuthorEdit}
      create={AuthorCreate}
      icon={AuthorIcon}
    />
  </Admin>
);

これによって生成される新規作成や編集画面は以下のようになります。BookのauthorフィールドはAuthorへの外部キーなので、AuthorレコードのnameからAPIを通じて引っぱってきて選択肢として表示されています。

f:id:uehaj:20171223223211p:plain

その他

React 16を使用するので、異常時の結果を判りやすくするためのコンポーネントErrorBoundary.jsこちらを参考にして使用します。

また、デフォルトのAdmin-on-Restのページネーションでは全ページへのリンクを表示できるがその機能を除去したバージョンのページネーションUI Pagination.jsを作成します。

デモ

オンラインデモ準備は間に合いませんでした。できたらリンクはります。 ソースコードこちらにあります。

まとめ

以上のように、ページベースアプリではなく、Web API化することで、クライアントはGrailsとは完全に独立したものを使用できるようになるわけです。Admin-On-Restは発展甚しく、機能も汎用性も高いので、是非使っていきたいところです。

もっとも、去年はJSON Schemaでドメインクラスからメタ情報を抽出し、表の項目やフォーム画面なのカスタマイズ、バリデーション処理なども生成することを試みました。今年はそこが抜けていて、フィールドの順序やフォーム中のデータ項目を明示的に指定していく必要はあります。

GraphQLはどこへ行ったの

GraphQLはRESTfulのカウンターとして出てきた技術で、高機能高効率なクエリを可能にすると同時にAPIを強く型付けするものです。

GrailsにはGORM GraphQLプラグインがあり、ドメインクラスを元にして、QraphQLスキーマの生成や、grapql browserの組込み、さらにはGraphQLクエリに対するハンドラの実装を行うことができ、一般的なサーバサイドのGraphQLソリューションが実現されています。

さらに、前述のようにAdmin-On-Restはapollo-clientをベースとしたGraphQLコネクタ(aka RestClient)を利用可能です。

ですので、当初のアドカレの予告タイトルではGraphQLでやったるで! というのを狙っていたのですが、調べていく内に(時間がなくなった!というのもありますが)、今いまでは、管理画面程度のデータの列挙や編集のために、GraphQLを適用するのはかならずしもベストではないな、と思っています。

その理由は、なんだかんだいってRESTful APIは、ページネーションとかは微妙に違がっていても、さまざまなサーバをまたがって共通的なモデルではあるわけです。カスタマイズもたかがしれてます。

GraphQLは、高機能でありもちろんページネーション的なものを実現可能ですが、いくつかの流儀があって、つまりRelayなのかGraphcoolなのか独自なのか、といった屋上屋的な規約が必要です。GrailsのGORM GraphQLプラグインは素のGraphQLしかないので、たとえばGraphchool規約に従うようなGraphQLマッピングを追加する、もしくは逆にGraphQLコネクタをaor-simple-graphql-clientなどを元にして修正していく必要があります。(ここらへん、GraphQLに詳しくないので嘘言ってるかも! 自信なし)

でもそういう接続を自分でやったとしてもその選択がメジャーになるのかはわからないし、今後Grails側かクライアント側かで発展して、GraphQLがアウトオブボックスで簡単に繋がるプラグインかコネクタが今後できてくるんではないかと踏んでいます。

そういうわけで、現時点でGrailsで管理画面を実装する程度では、RESTfulを採用した方がリーズナブルで現実的であり、特に他の理由でGraphQLサーバを仕立てたいのでない限りは、RESTfulで良いと考えています。

なお、GORM GraphQLプラグインはまだ使い込んではいませんが、GraphQLのカスタムオペレータやフェッチャーを定義する形態として、データと表裏一体に定義できる、GrailsのGORM上のDSLというのは筋が良いと思っています。

ポエム

過去にフレームワークと呼ばれたものは、構成要素に分解され、疎結合化されていく時代です。分解された要素はマイクロサービスで実現されるかもしれないし、クラウドプロバイダーが提供する機能や、FaaSで代替されるのかもしれません。要するに、フレームワークが分解し、「クラウドプラットフォーム全体が大きなフレームワークになった」のです。

そのような現代および未来のプログラミングの主眼はAPIの接合にあり、UI部はSPAがまかないます。この記事シリーズは、そういう流れでフレームワーククラウドを捉えています。

Reactがなぜ重要か

この観点からは、まずUI部に関して、今回のAdmin-On-Restを含むさまざまなイノベーションがReact界隈から連続的に生まれてきています。Reactは今後も圧倒的に注目すべき技術であると思います。個人的にはMaterial-UI-Next、React Nativeが注目です。

GrailsとはGORMである

そしてGrailsをこの「クラウド=分解されたフレームワーク」論から見ると、Grailsの価値はデータ表現のDSL、ORMが残ると言えます。GORMを書きやすく扱いやすくするシステムがGrailsなのです。GORM GraphQLプラグインはその応用例の一つです。

みなさま、ではメリークリスマス and 良いお年を!

一年が過ぎ

一年hatenablog投稿してませんでしたね。 Qiitaに投稿してた1年分の記事を参照用にリンク貼っておきます。

qiita.com qiita.com qiita.com qiita.com qiita.com qiita.com qiita.com qiita.com qiita.com

Grails React Scaffoldを目指すざます

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

去年のG*Advent Calendarでは、本ブログではReact Meets Grails 〜ReactはエンタープライズSPAの夢を見るか?〜 - uehaj's blogと題して、バックエンドを「GrailsのRestサーバ」+「React SPAによるフロントエンド」という組合せをボイラープレート(雛形)として実装してみました。

今年もこの路線を拡充してみます。

背景

GrailsにはScaffold機能、すなわちCRUD操作を行なうためのWeb画面の自動生成機能があり、確かに便利なものではあるのですが、以下の点では不満がありました。

ページナビゲーション・ルーティング機能不足
業務Webアプリ開発で必要になることが多い階層メニュー選択やページナビゲーション機能がそのままではサポートされません。 React Routerのように画面構築と連動したルーティング機能が利用できません。
ページ遷移ベースであること
データの各操作1つ1つ(テーブルのレコードを参照、編集、削除...)などにすべてページ遷移が伴ないます。
2-way編集の困難さ
Scaffoldで生成されたGSPやコントローラを何らかの理由で手で修正してしまうと、ドメインクラスを修正したときの再生成に問題がおきたり、もしくは手動での取り込みが必要になります。かといって動的Scaffold、Fieldsプラグインなどでカスタマイズしようとすると後述のカスタマイズの困難さに直面します。
カスタマイズの困難さ
Scaffoldの元になる「テンプレート」や部品のレンダリングテンプレートを修正することができますが、GSPはもともとHTMLを生成するテンプレートのようなものなので、「HTMLを生成するテンプレート(GSP)を生成するテンプレート」を書く必要があり、さらに分離されたJavaScriptのコードは生成の埒外なので、それとの連携を考えると3段階を考慮したプログラミングとなり、難易度が高く保守性の低いものとなります。結果的に、たとえば「顧客ごとにカスタマイズした別ビュー」を提供するなどが難しくなります。
JavaScriptと連携したときの再利用の困難
GSPにある「ページフラグメント」「タグライブラリ」などは単純なHTML断片を共通化するのには有効なのですが、動的なDOM構築が必要になったときに破綻します。JSを組合せたとき、画面部品の再利用や、保守性の高い構造を達成し保守するのが困難です。(サーバでのHTML生成がかかえる本質的な困難)
jQueryの問題
そもそもjQueryベースだと保守性を高くすることが難しい
GUIの自動試験のしにくさ
RESTfuul APIならコントローラも試験しやすくなる。またクライアントサイドのJSで利用できるReact Storybookなどの便利な画面部品試験ツールが利用できない。
クライアント・サーバ間の密結合
ビューだけの修正時にもアプリのwarパッケージの生成・再デプロイの必要がある。その結果、ライフサイクルが不一致な場合(たとえばiOSのビューを修正するときにAndroid版も止めなければならない)などの問題がある。
Webアプリとネイティブアプリ(モバイル+デスクトップ)間でのコード再利用・共有が困難
React NativeIonic2のような仕組で共有することはできない。

これらのほとんどは、GrailsのScaffoldの問題であると同時に、一般にページ遷移ベースそのもの、そしてアプリに自動生成を適用することの問題でもあります(つまりRailsなどでも同様)。結果として、Scaffoldの実システム開発での利用は管理画面(admin ui)などに留まり、コンテキスト依存サンプルコードとして有用ではあるものの、限定的でした。

解決策

前述の問題は一般的であるが故に、各所でSPAフレームワークやツール等が解決しようと努力なされてきました。その混沌たる候補の中から、ここ数年で突破口が見えてきた気がします。すなわち

  • サーバはRESTサーバなどのWeb APIサーバとする。
  • 画面はSPAフレームワークを使いJSで作り込む

上記により多くが解決・改善されます。このことを踏まえて、GrailsのRESTfulサービス開発機能を併用し、開発のベースとなるようなReactとの連携コードサンプルを提供するのが今回記事で紹介している「React Grails Boilerplate」の目的です*1

上記のような課題を克服することで、実世界アプリ開発を楽にできるようにすることを目指しています(将来的には!)。

作ったもの

以下画面です。

f:id:uehaj:20161222062739g:plain f:id:uehaj:20161223000101g:plain

ソースはこちら。

実行方法

% git clone  https://github.com/uehaj/grails3-react-boilerplate.git
% cd grails3-react-boilerplate
% ./gradlew bootRun

bootRunではサーバとクライアントは別プロセスになっていて、並列に動作させています。個別に起動する場合は以下のとおり。

% cd server
% ./grailsw run-app &
% cd ../client
% npm start

去年からの進展

主には以下のとおりです。

特徴

売りとしては、JSON-schemaをGORMドメインクラスから自動生成することで、以下を可能としていることです。

  • json-schemaを扱えるフォームライブラリreact-forms-jsonschemaにより、入力フォームの自動生成
  • GORMのドメインクラスの制約(constraints)をクライアントサイドの入力フォーム自動バリデーションに自動変換
  • ドメインクラスやフィールドの変更追加に対して自動追随

つまりどういうこと?

DBのフィールド増減の際に何箇所も直してまわったり、バリデーションに関しての同じ処理をサーバとクライアントの二箇所で別言語で書いたり*2、整合性を失なうようなことを避けることができます。 もっともこれは従来のScaffoldでも一部できていたことで、JSON-Schemaで汎用的に簡単にできたよ、ということです。

加えて、ドメインクラスの一覧をページ表示時に動的に取得し、対応するメニュー項目、react routerのRoute階層などを自動生成します。

JSONSchemaとは

JSONデータ用のデータスキーマです。バリデーション情報を含んでいます。これにFORMなどの見た目情報を追加する、uiSchemaというものも定義されています。

{
    "title": "Example Schema",
    "type": "object",
    "properties": {
        "firstName": {
            "type": "string"
        },
        "lastName": {
            "type": "string"
        },
        "age": {
            "description": "Age in years",
            "type": "integer",
            "minimum": 0
        }
    },
    "required": ["firstName", "lastName"]
}

このスキーマをフォームとして表示、入力、バリデーションができるReactのライブラリが ract-forms-jsonschemaです。

今後やりたいこと

  • grails application profile化
  • 関連の実装
  • 国際化
  • pageneteをきちんと実装してレコード数の制約を解除(今はクライアントサイドに一気読み込みのなんちゃってページネーション)
  • webpackのhot module replacement対応
  • grails constraintsに対応付かないJSONSchema、および uiSchemaをドメインクラス側で明示的に指定するための仕組みの追加。カスタムconstraintsを追加する、生のJSONScehma/uiSchemaを返すためのメソッドをサポートするなど。

まとめ

私がReactに注目しはじめたのは2014年頃とやや遅かったのですが、かれこれ2年たち、Reactは順調に発展・普及し、成熟してきました。今後もますます業務開発で採用される機会も増えてきた/来るのではないでしょうか。 Reactは本来的にはプログラマにとってとっつき易いものですが、それなりの構造を作るのはやはり難しいので、このボイラープレートがサーバサイドプログラマがフロント開発をするための役などにたてば嬉しいです。

ではみなさん、メリークリスマスイブイブ!

入門 React ―コンポーネントベースのWebフロントエンド開発
Frankie Bagnardi Jonathan Beebe Richard Feldman Tom Hallett Simon HØjberg Karl Mikkelsen
オライリージャパン
売り上げランキング: 46,989

*1:ちなみにAngular scaffold profileは既にあります

*2:もっとも、Grailsで言うvalidate:クロージャをクライアントで実行する、などはできない。

ParrotブランチでGroovy3を垣間見る

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

はじめに

Groovyの構文解析処理には初期からANTLRというパーサジェネレータのバージョン2(antlr2)が長らく使われてきたのですが、最近Daniel SunさんによってANTLRのバージョン4(antl4)への書き直しがなされました。この新パーサーは"Parrot"と呼ばれています。ParrotのGroovy本体への組込みは、Groovyソースコードの'parrot'ブランチで作業が進められています。

Parrotでは従来との互換を損なわないように全構文が実装されていることに加え、構文レベルでの新たな機能追加がなされており、本記事で紹介します。

Parrotは、構文解析の結果として従来と互換性のある中間コード(抽象構文木)を吐くように設計されており、parrotブランチではすでに実際に新パーサでGroovyコードを実行できるようになっています。

構造上parrotは旧版パーサとコンフリクトせずに同時に組み込んでシステムプロパティで切り換えて使うことができるようになっています。またsubprojectとしてモジュール化され分離されています。parrotが実際にGroovyに組込まれる予定はわかっておりません。記事タイトルは「Groovy 3」としましたが、利用可能になる時期はひょっとしたらもっと早いかもしれません。

Parrotブランチのコンパイル

以下の手順でParrotを試すことができます。

$ git clone https://github.com/apache/groovy.git
$ git checkout parrot
$ ./gradlew -PuseAntlr4=true -x groovydoc -x test installGroovy
$ export GROOVY_HOME=$(PWD)/target/install
$ export JAVA_OPTS=-Dgroovy.antlr4=true

$ $GROOVY_HOME/bin/groovy
$ $GROOVY_HOME/bin/groovysh
$ $GROOVY_HOME/bin/groovyConsole

警告

parrotはα以前の段階ですし、将来リリースされたときに仕様が本記事のままである保証は全くありません。また、さらに多数の機能拡張がなされていくことも予想され、期待されます。

do-whileループ

まずはこれ、Javaにはあるdo-whileループが従来Groovyにはありませんでしたが、利用できるようになってます。

groovy:000> do { println i++ } while (i<10)
0
1
2
3
4
5
6
7
8
9

do-while結構使いますよね。待望の、という感じです。

Java8構文

ParrotはJava 8で拡張された構文のいくつかに対応しています。

ラムダ式の記法

筆頭としてラムダ式です。Groovyでラムダ式の記法が使えるようになりました。*1

def list = [2, 3, 1]
Collections.sort(list, (n1, n2) -> n1 <=> n2)
assert [1, 2, 3] == list

今のところ、セマンティクスは従来のGroovyクロージャと同様です。parrotでのラムダ式のような記法「(a,b,c) -> body」は、クロージャ「{a,b,c -> body}」のシンタックスシュガーと思えばよいでしょう*2

それを確認するためにGroovyConsoleでinspect ASTしてみます。

f:id:uehaj:20161216004742p:plain

上記のように、コンパイル処理の早期段階である「conversion」ですでにクロージャに変換されていることがわかります。

なので、たとえばラムダ式記法を囲むメソッドのローカル変数をクロージャ中から変更することができますし、.apply()とか呼ばなくても()で呼べますし、delegateも機能しています(今のところ)。また、Java8のラムダ式のようにinvokeDynamicに変換されるということも(今のところは)無いでしょう。

細かい話1

Groovyのラムダ式は基本的にJava8のラムダ式に対して構文的には同等もしくは上位互換です。 たとえば、Groovyでも本体ブロックの括弧を省略したりできますし、さらにGroovyの特性としてreturn省略もできます。

(a,b) -> { return a+b }
(a,b) -> { a+b } // Groovyの仕様により許可(Javaではreturnが無いと怒られる)
(a,b) -> a + b // これならJavaでもreturn無しにできる。もちろんGroovyでもOK。
(int a, int b=0) -> a+b // デフォルト引数はGroovyならでは

しかしながら、わずかに例外もあります。 Javaでは引数が1個の場合、引数の括弧を省略できます。

(a) ->  a+1
a ->  a+1 // こう書ける。

しかし、parrotでは引数が1個でも括弧を省略できません。理由は、「{ a-> a+1 }」が、「クロージャ」なのか、「ラムダ式を含むブロック」なのかが構文解析上判別できなくなるからだそうです。=>なら区別できただろうに、先見的に->を使ってたのがかぶったという…。

細かい話2

クロージャで使えた暗黙の引数「it」はラムダ式では使えません。

groovy:000> [1,2,3].each ()->System.out.println(it)
ERROR groovy.lang.MissingMethodException:
No signature of method: groovysh_evaluate$_run_closure1.doCall() is applicable for argument types: (Integer) values: [1]
Possible solutions: doCall(), findAll(), findAll(), isCase(java.lang.Object), isCase(java.lang.Object)
groovy:000> [1,2,3].each (it)->System.out.println(it)
1
2
3
===> [1, 2, 3]

穏当です。

メソッド参照の記法

Java8のメソッド参照の記法が使えます。こちらも同様に、Groovyの.&演算子の適用のシンタックスシュガーで、つまり「System.out::println」は「System.out.&println」と同様です*3

ところで細かい話ですが、 Java8のメソッド参照で「クラス::インスタンスメソッド」と指定すると、第一引数がそのメソッドのレシーバであるような関数を返すのに、 今までのGroovyでは.&演算子で「クラス.&インスタンスメソッド」を指定したとき、そのメソッドに対してレシーバを渡す方法がなく呼び出すことができない、という違いがありました。

具体例を挙げます。Java8のメソッド参照では「クラス::インスタンスメソッド」を指定すると、

a = String::toUpperCase
assert a("abc") == "ABC"

のようにレシーバを第一引数で与えるように変換した関数を返すので、高階関数に渡したりするのにたいへん便利です。 しかし従来のGroovyでは「クラス.&インスタンスメソッド」を指定した場合、

groovy:000> a = String.&toUpperCase
===> org.codehaus.groovy.runtime.MethodClosure@2f67b837
groovy:000> a()
ERROR java.lang.IllegalArgumentException:
object is not an instance of declaring class
groovy:000> a("abc")
ERROR groovy.lang.MissingMethodException:
No signature of method: java.lang.String.toUpperCase() is applicable for argument types: (java.lang.String) values: [abc]
Possible solutions: toUpperCase(), toUpperCase(java.util.Locale), toLowerCase(), toLowerCase(java.util.Locale)

のようにエラーになって呼び出せませんでした。「インスタンス.&インスタンスメソッド」「クラス.&静的メソッド」は可能なのですがね。

parrotでは、Java8のメソッド参照形式でも「インスタンス::インスタンスメソッド」は当然できるし、ついでにGroovy形式の.&で「クラス.&インスタンスメソッド」を指定したとき

a = String.&toUpperCase
a("abc") == "ABC"

が可能になります。つまり.&はJava8のメソッド参照と同様のものになったということです。

コンストラクタ参照

こちらも可能となりました。勢い余ってかメソッド参照(.&)でも同じことが可能になりました。

groovy:000> s = String::new("abc")
===> abc
groovy:000> s = String.&new("abc")
===> abc

default method

これもかなりインパクトのあったJava8で導入された機能でした。

groovy:000> interface Foo { default getName(){ "foo" } }
===> true
groovy:000> class Bar implements Foo { }
===> true
groovy:000> new Bar().getName()
===> foo
groovy:000>

traitと同時にimplementsしたときどうなるのか、とか興味深いですが試してません。 ちなみにJava8ではインターフェースでstaticメソッドが定義可能になりましたが、それはまだ実装されていないようです。

Java7対応

try-with-resources

できます!

% cat a.groovy
class Resource implements Closeable {
  void close() { println "close" }
}

try (r = new Resource()) {
  println "hello"
  throw new Exception()
} catch (Exception e) {
  println "catch"
}
% $GROOVY_HOME/bin/groovy a.groovy
hello
close
catch

新しい演算子たち

等値演算子(===, !==)

意味的には、===はObject.is(Javaでの==)、!==はその否定ですね。

groovy:000> "a"+"b"=="ab"
===> true
groovy:000> "a"+"b"==="ab"
===> false
groovy:000> "a"+"b" != "ab"
===> false
groovy:000> "a"+"b" !== "ab"
===> true

エルビス代入演算子(i.e. ?=)

xx  = xx ? : 3

の略記法として、

xx  ?= 3

と書けます。「変更しようとする変数に値が設定されていなかったら設定する(先勝ち)」です。こんな動きをします。

groovy:000> def foo(xx) { xx ?= 3; println xx }
===> true
groovy:000> foo(null)
3
===> null
groovy:000> foo(5)
5
===> null
groovy:000> foo(0)
3
===> null

GroovyTruthで判定するので0は負けます。

!in, !instanceof

これは、私にとって今回の一番のお気にいり演算子です。

groovy:000> 3 !in [1,2,4]
===> true
groovy:000> 3 !in [1,2,3,4]
===> false
groovy:000> "a" !instanceof Integer
===> true
groovy:000> "a" !instanceof String
===> false

読みやすく書きやすい。

参考資料

以下の前者は記事をほぼ書きおわった後発見してちょっとショック。

まとめ

Groovyは本来Javaの上位互換言語であり、「任意のJavaコードは(ほぼ)Groovyコードでもある」が売りの一つでした。今回のパーサで導入されたJava8、Java7 の互換向上機能は、しばらくの間、いくぶん損なわれてしまっていたそれらのメリットを再度取り戻すものであるでしょう。また、今後の機能拡張の基盤ともなるものです。

コミッターも増え、Apache Groovy開発が活性化してきております。来年の発展が楽しみです。

プログラミングGROOVY
プログラミングGROOVY
posted with amazlet at 16.12.15
関谷 和愛 上原 潤二 須江 信洋 中野 靖治
技術評論社
売り上げランキング: 339,124

*1:昨日の記事の伏線はこれです。

*2:だから、「parrotではJava8のラムダ式を導入した」は言いすぎだと思う。穏当なのは「Java8のラムダ式の記法でもクロージャが記述できるようになった」ですかね。

*3:なので内部的にMethodHandleを使ってないので、これもJava8のメソッド参照とは構文上酷似した別モノ、ってことになるのかもしれない。

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:帰りに杉本さんに教えていただきました。ありがとうございました