関数型的なJS

JavaScriptで学ぶ関数型プログラミング

JavaScriptで学ぶ関数型プログラミング

ECMAScript6まじりのJavaScriptに手が馴染んできました。Reactがclass構文をサポートしながらも、現バージョンの0.14.xではまだmixinがサポートされていないためほぼそこだけはReact#createClass()を用いてます。これはreact-routerもmaterial-uiもそうしてるんで今のメジャーソリューションなんだと思います。他はBabelで通る限り極力6で書く。手に馴染んでくると、朧げながらもベストプラクティスが見えてくるものだと思います。ということで。。。

Reactで可変長のリスト表示を作る時によく見るmapがきっかけでしたが、その後にもすでにECMAScript5からArray型にreduceだとかforEachだとか標準で入ってたことを知り、関数をダイナミックに作って返すなんてのはPassport.jsなどのExpressミドルウェアライブラリでは至極よく見る作りだったのでこう言った関数型的なところのプラクティスを一度ちゃんとさらっておきたいなあと思って、正月に日本に帰った時に「JavaScriptで学ぶ関数型プログラミング」を買ってきてました。JavaScriptじゃない言語で読んでも疲れちゃうなあと思ってたのでこの本見つけた時には喜んだ。で、読んだ。。。うざくて読みずれー。

5が見えてるけど普及がこれからの時期に3前提でUnderscoreを用いて説明してるためにちょっと迂遠なのと、各所で「すごいでしょう?これが関数型のパワーなのです」ということを修辞を変えて繰り返し繰り返し繰り返し繰り返し(中略)繰り返しのために、大事なことと大事じゃないことがごっちゃになる。

JavaScriptパターン ―優れたアプリケーションのための作法

JavaScriptパターン ―優れたアプリケーションのための作法

実は「JavaScriptパターン」第4章:関数の方が概要大体のことを得ることができます。「関数型プログラミング」の方はいろいろ丁寧に書いているし、より高度なのにもったいない。手頃かつ他にない良書の素質があったのに無駄なウザさで減点。でも総合点では良書かなあ。この本を読んでからはreduceRightとか使うようになりました。

ECMAScript6で学ぶ関数型プログラミング、がすでに世の中みんな関数型っぽく書いているよねーって前提で淡々と書かれたら神書の予感。

別の話だけど、結構Reactで書けるようになってきたので、すぐにはやらないけどReact Nativeもちょっとさらっておきたかった。

Learning React Native: Building Native Mobile Apps with JavaScript

Learning React Native: Building Native Mobile Apps with JavaScript

日本語の訳書が出てなかったら原書を紙で買います。ここは米国なんで在庫も豊富で安いし、ちょっと読んでダメだったら返品できる。それにしても私はオライリー好きなのかな。オライリーじゃなきゃManning(〜in Actionって仮装した人が表紙になるやつね)か。一緒に買ったけどまだ届かないのも、どうやらオライリー関係会社っぽい?なぜか出版社のWEBサイトが一緒。

Mongodb Data Modeling

Mongodb Data Modeling

追記

GitHub - brigand/react-mixin: mixins in react with es6 style classes

react-routerのドキュメントでたまたまこちらへ誘導されましたが、ES6-classでMixinを実現しましょうライブラリ。さらに承前としてラッパーコンポーネント的にmixin機能を代替する記事 (https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750#.nlylwdx3d) 。おや?Reactはmixinをなかったことにしたい?現バージョンのドキュメントにはその痕跡見つからなかったけど。。。同じく将来も残るか怪しいって公式ドキュメントに書かれているcontextと共にバリバリ使ってるんで、ちょっと悩ましい。タイムラグのために最新では議論が違う方に行ってるのであればいいんだけど。react-routerがv2からmixinを廃止推奨してきたのはそのためか?代替案がcontext利用なので、それもまた悩ましいはずなのですけど。

とはいえ、ラッパーコンポーネントのイディオムはreact-side-effect(https://github.com/gaearon/react-side-effect/blob/master/src/index.js)のソースコードでも見て、ちょっと賢いと思ったので、使い方考えてみよう。

MongooseでのCRUDと謎挙動

const Mongoose = require("mongoose");
const Schema = Mongoose.Schema;
Mongoose.connect("mongodb://localhost:27017/sample");
const Person = Mongoose.model("person",  // <-この"person"が後で問題になる
    new Schema({
        created: {type: Date, default: Date.now},
        name: String
    })
);
export {Person};

こんな風に適当なPersonスキーマを作って、

const Express = require("express");
import {Person} from "./Models";
const APIRoute = Express.Router();
APIRoute.post("/person", (req, res) => {
    let upsertPerson = new Person(req.body);
    Person.findOneAndUpdate(
        {_id: upsertPerson._id},
        upsertPerson,
        {upsert: true},
        (error) => { /*割愛。*/ res.sendStatus(200);}
    );
});

APIRoute.delete("/person", (req, res) => {
    let deletePerson = new Person(req.body);
    deletePerson.remove((error) => { /*割愛。*/ res.sendStatus(200);});
});

APIRoute.get("/person", (req, res) => {
    Person.find().limit(10).sort({_id: -1})
        .exec((error, list) => {/*割愛。*/ res.send(list);});
});

export default APIRoute;

expressの本体は以下。

const Express = require("express");
const bodyParser = require("body-parser");
import APIRoute from "./routes/APIRoute";
app.use(bodyParser.json());
app.use("/api", APIRoute);
app.listen(8080);

送信側は、Reactのコンポーネントの中から抜粋。

//前略
    changePerson(person, remove) {
        const method = remove ? "del" : "post";
        SuperAgent[method]("/api/person")
            .set("Content-Type", "application/json")
            .send(person)
            .end((err, res) => {
                if (res.statusCode == 200) {
                    this.loadList();
                } else {
                    this.setState({error: err});
                }
            });
    },

    loadList() {
        SuperAgent.get("/api/people")
            .end((err, res) => {
                if (res.statusCode == 200) {
                    this.setState({list: res.body});
                } else {
                    this.setState({error: err});
                }
            });
    },
//後略

これでちゃんとデータのCRUDができますが。。。謎挙動が一つ。こんだけ"person"を連呼しているのに、Mongo上に作られるCollectionの名前が複数形の"people"になる!これが仕様なのですが、わざわざ何でこんなことをするのか、mongooseの実装ポリシーがわからない。あまりにも謎すぎてググったら、やはり皆が頭を悩ませていました。例えば"Activity"を指定したら"Activities"になるし、単数形と複数形が同じ単語の場合は変換されないという無駄に芸が細かい。

ということで、冒頭のスキーマコードではCollection名を設定する以下の箇所を正すべき?正さないべき?直さなくても動きますし、mongoの実際のコレクション名と設計は別のことですけどね。直さない方がいいのかなぁ。

const Person = Mongoose.model("people", // <-"person"を"people"に変更?しなくても良い

material-uiのi18n対策のひとつ

Material Designを実現するReactコンポーネントセットであるmaterial-uiは有用な上で、ソースを読んでも大変参考になるものが多くあります。中でもDatePickerのコードはReactでの動作エフェクトの作り方の他、コンポーネントを組み合わせてコンポーネントを作るというReactの基本的なことまで、多様なテクニックの良質なサンプルともなってます。しかしそんなmaterial-uiもL10nについては手が付いてません(v0.14.2現在)。

https://github.com/callemall/material-ui/blob/v0.14.2/src/utils/date-time.js

  warning(locale === 'en-US',
    'Wrong usage of DateTimeFormat. The ' + locale + ' locale is not supported.');

上記リンクの日付フォーマット関数を見れば様子がわかろうというもの。おいおい中身は"en-US"一択かよ。つまりは一応i18nのきっかけは作ってくれてるけど、中身の実装が追いついていないようです。コア開発者は昼間の仕事としてやってる風で日々の進化も早く、磨かれていってる感が週単位ぐらいで見て取れるのでそのうち対応するのでしょう。

こちらはなんちゃってで日本語対応だけしてみました。中国語と英語はライブラリに丸投げです。

const moment = require("moment");
require('moment/locale/ja');
require('moment/locale/zh-tw');

// material-uiのDatePickerコンポーネントの表示を国際化する
function MomentFormat(locale, options) {
    this.format = function(date) {
        var format1 = "ddd, MMM DD";
        var format2 = "MMMM YYYY";
        var format3 = "MMM/DD/YYYY";
        if(locale && locale.startsWith("ja")) {
            format1 = "M月D日, dddd";
            format2 = "YYYY年 M月";
            format3 = "YYYY年M月D日(ddd)"
        }
        let m = moment(date);
        m.locale(locale);
        // 将来への意欲は感じるが、現時点は無意味なmaterial-uiのoptions!
        if(!options) {
            return m.format(format3);
        } else if (options.month === 'short' &&
            options.weekday === 'short' &&
            options.day === '2-digit') {
            return m.format(format1);
        }
        return m.format(format2);
    };
}

export default MomentFormat;

こんなのを書いて、次のように使う。formatDateはプレースホルダ用でDateTimeFormatとlocaleは入力ダイアログ用。

<DatePicker autoOk={true} wordings={{cancel: this.state.captionCancel}}
                      formatDate={new MomentFormat(this.state.locale).format}
                      DateTimeFormat={MomentFormat} locale={this.state.locale}/>

で、こうなる。ここまでやってもダイアログの最上部の年表示は仕込めず数字だけ。「2016年」と年だけの表示でもi18nすべきことは米国人には想像できなかったか。

f:id:masataka_k:20160125054915p:plain

日付のL10nでmoment.jsを使ってますが、こちらのキモはロケールの読み込み方法です。

require('moment/locale/ja');

これをやらないとmoment.locale()が空っぽの関数のまま。requireして初めてlocale("ja")が効くようになります。アプリケーションとして対応するロケール毎に同じことをやってあげれば良いのですね。私は全く読めませんがアラビア語などもmoment.jsは対応しています。

追記 v.0.14.3

さて、後日(1/27)にmaterial-uiをv.0.14.3にアップデートしたらDatePickerもちょっと進化しました。

f:id:masataka_k:20160128152944p:plain

なんと!曜日がi18nされた!ただし相変わらずen-US以外へのL10nはされてない。そのため、以下に書き直し。"narrow"なるOptionが増えてましたので、そちらに対応しました。

"use strict";
const moment = require("moment");
require('moment/locale/ja');
require('moment/locale/zh-tw');
function MuiDateFormat(locale, options) {
    this.format = function(date) {
        var format1 = "ddd, MMM DD";
        var format2 = "MMMM YYYY";
        var format3 = "MMM/DD/YYYY";
        if (locale && locale.startsWith("ja")) {
            format1 = "M月D日 dddd";
            format2 = "YYYY年 M月";
            format3 = "YYYY年M月D日(ddd)"
        }
        let m = moment(date);
        m.locale(locale);
        // ガンバレ、その意気だ!
        if (!options) {
            return m.format(format3);
        } else if (options.month === 'short' &&
            options.weekday === 'short' &&
            options.day === '2-digit') {
            return m.format(format1);
        } else if (options.month === 'long' &&
            options.year === 'numeric') {
            return m.format(format2);
        } else if (options.weekday === 'narrow') {
            return m.format("dd");
        }
    };
}
export default MuiDateFormat;

Reactコンポーネントをファイル分割したら沈黙してしまった件

Reactで大きな画面を作るうちに、人の習性としてReactコンポーネントをファイル分割したくなります。

var React = require('react');

var Item = React.createClass({
    render: function () {
        return <li value={this.props.value}>{this.props.text}</li>;
    }
});

var List = React.createClass({
    render: function() {
        var items = this.props.data.map(function(i) {
            return <Item {...i}/>
        });
        return (
            <ol>{items}</ol>
        );
    }
});

var data = [
    {text: 'One', value: 1},
    {text: 'Four', value: 4},
    {text: 'Five', value: 5},
    {text: 'Seven', value: 7}
];

var ReactDOM = require('react-dom');
ReactDOM.render(<List data={data}/>, document.getElementById('List'));

と、まあこんなサンプルがあったとして、気持ち悪いのはコンポーネントの宣言記述と、その利用が同じファイルでやってること。Reactの説明で多くの場合はコンポーネントの宣言直下でReactDOM.render()を行ってますが、まあ実践では違う作りになるんじゃないかなと。で、以下みたいにするとします。

// List.jsx
var React = require('react');

var Item = React.createClass({
// 省略
});

module.exports = List = React.createClass({
// 省略
});
  • 描画部の別ファイル
var data = [
// 省略
];

var ReactDOM = require('react-dom');
var List = require('./List.jsx');
ReactDOM.render(<List data={data}/>, document.getElementById('List'));

汎用たるコンポーネントと、個別たる描画利用部が分かれていい感じ。requireもreactとreact-domがそれぞれで、なるほどこれがデザインかと。しかしこれらをBrowserifyでまとめたらさっきまで動いていたのに沈黙してしまいました。

しばらく嵌るうちにわかったのは、Listコンポーネントがexportされているけど、その中で使われているItemコンポーネントがexportされていないから。全部同じファイルだったら見えるし、描画-List-Itemと三つのファイルに分割する(もちろんItemも適切にexportする)のならば動きます。ただし描画部で、ReactDOM.render()の引数に渡しているJSXがTranspileした結果、React.createElement(List, {data: data})と変換されているので、Reactをコード上でてこなくてもrequireしなくちゃならなそうだったりと新たな痒さも。

コンパイラ言語だったり、JavaScriptでもクロージャーみたいに、スコープ見えるはずと思っていてもBrowserifyによる力技なコピーかき集め作業の前には通用しなかった。Reactコンポーネントはファイル毎にひとつづつってのがReact-Lintも警告するような常識だったようですが、かといって細かいファイルたくさんは作りたくない。。。はてさて。

document.querySelector

Reactコンポーネントをページに適用する際に、たったこれだけのためにjQueryを入れたくもなかったので、document.getElementByClassNameや〜Idでエレメント取得していましたが、単純名だけかい!という課題に私が知らなかっただけですでに高度な回答がありました。document.querySelector(CSSセレクター式)。

そもそもにCSSセレクターを叩くメソッドがあったのね。ブラウザ適用状況も、大体モダンなやつは行けてます。ただ残念なのはドキュメントの注釈に、getElementsByClassNameや〜Idの方が早いよーと明言されていること。結局は一周して元に戻る。

PassportによるFacebook認証(続いてDBへ保存)

Passportでの認証を実践的に進めてみます。認証情報を永続化するのにMongo DBを利用します。はじめだけMongo DBのJSドライバをそのまま使って書いてましたが、コードブロックをtry-finallyで囲って最後にDB接続を解放するような書き方など2000年ぐらいにJava出始めでJDBCで接続ガーと言ってる頃を思い出しました。懐かしいのは良いことばかりでもなく、何事もイージーにライトでやりたい現代のスピード感にそぐわないのですぐにMongoose通じてアクセスするように。Mongooseを選んだのは単に軽くググったらたくさん記事が出てきたのでメジャー感あったためです。アーキテクチャはいわゆるActive Record的なアレですね。機能は十分に豊富ですが、とは言えシンプルです。

var Mongoose = require('mongoose');
Mongoose.connect('mongodb://localhost:27017/buzzw');
var Account = Mongoose.model('accounts', new Mongoose.Schema({
    provider: String,
    id: String,
    displayName: String,
    accessToken: String,
    lastLogin: Date
}));

var Passport = require('passport');
var Facebook = require('passport-facebook');
Passport.use('fb', new Facebook.Strategy({
        clientID: '1513567018936647',
        clientSecret: '2596-xxxxxxxxxsecretxxxxxxx-d126',
        callbackURL: "http://localhost:8080/auth/facebook/callback"
    },
    function(accessToken, refreshToken, profile, done) {
        Account.findOne({id: profile.id}, function(err, account) {
            if(err) {
                return done(err);
            }
            if(!account) {
                // Active Record的なオブジェクトのコンストラクタにドメインオブジェクトを渡す
                account = new Account(profile);
            }
            account.accessToken = accessToken;
            account.lastLogin = new Date();
            account.save(function(err) {
                return done(err, account);
            });
        });
    }
));

Passport.serializeUser(function(user, done) {
    done(null, user._id);
});

Passport.deserializeUser(function(obj, done) {
    Account.findOne({_id: obj}, function(err, account) {
        done(err, account);
    });
});

var Express = require('express');
var app = Express();
app.disable('x-powered-by');
app.use(Passport.initialize());
app.use(Passport.session());
app.get('/auth/facebook', Passport.authenticate('fb'));
app.get('/auth/facebook/callback', Passport.authenticate('fb',
    {failureRedirect: '/index.html', successRedirect: '/success.html'}));
app.use(Express.static('public'));
app.listen(8080);

Active Record的なオブジェクトを作るのにSchemaを引数に渡しますが、こちらはnew付きの呼び出ししか対応していなかったので注意。最近の多くはnewをつけてもつけなくても同じ動きをするように一手間費やされているライブラリが多いのですが、ここにはそれがなく。 新規時にこのオブジェクトを作る時、つまりレコードをinsertする時にはFacebookから得られたドメインオブジェクトたるユーザー情報(profile)を渡してあげると、そのまま綺麗にDBへ格納してくれます。insertもupdateもコード的には区別なくいい感じ。

Mongooseでちょっと気持ち悪かったのは、connectで返すのがDBインスタンスではなくSocketで、インスタンスはMongoose.connectionというプロパティに入ってるってことかな。例外処理はDomain使った風に以下のように宣言します。

var db = Mongoose.connection;
db.on('error', function(err) {
    console.error(err);
});

APIユースケースとしては、connect呼び出し時にインスタンス返せばいいのにね。

x-powered-byを消す

悪意ある攻撃への対策として、Responseのヘッダから'x-powered-by'を削ると良いという。

Expressでは何もしなければ、サーバ実装を示すこのヘッダ値に'Express'と正直に返してしまいます。そうなると悪い人が「そうかNodeでExpress使ってるのね」とすぐわかってしまうので攻撃前の一手間が省かれてしまうという。いや、しかし、Node+Expressは一定以上使われてるメジャーなプラットフォームだと思うので悪意ある人の前では無駄な努力だとは思うけど...とりあえずおまじないとしてやっておく方がより良いということでしょう。

var app = express();
app.disable('x-powered-by');

これだけ。

そういえば、Node5.1.0にアップデートされていました。$sudo n stableではまだ対応されてなかったので$sudo n 5.1.0で上げました。npmは3.5.0です。