PHP でも関数型っぽく書く

なんとなく表題の事をやってみた。 とりあえず、

  • 関数合成
  • 部分適用

があればなんとかなりそうなので、作ってみる。

// 関数合成
function compose()
{
    $fns = func_get_args();
    $revFns = array_reverse($fns);
    return function ($initial) use ($revFns) {
        return array_reduce(
            $revFns,
            function ($acc, $fn) {
                return call_user_func($fn, $acc);
            },
            $initial
        );
    };
}

// 部分適用
function partial()
{
    $args = func_get_args();
    $fn = array_shift($args);

    return function () use ($fn, $args) {
        $newArgs = func_get_args();

        $allArgs = array_merge($args, $newArgs);
        return call_user_func_array($fn, $allArgs);
    };
}

できたので、いつもの偶数二乗和を求める関数を作ってみる。

// 偶数二乗和を求める
function evenSquareSum($n)
{
    // 最後の引数が配列になるように引数の順番を合わせておく
    $filter = function ($callback, $array) {
        return array_filter($array, $callback);
    };
    $map = function ($callback, $array) {
        return array_map($callback, $array);
    };
    $reduce = function ($initial, $callback, $array) {
        return array_reduce($array, $callback, $initial);
    };

    // 配列を一つ受け取り、偶数のみ抽出、各要素を 2 乗する、全ての要素を合計する関数を作成
    $selectEven = partial($filter, function ($n) { return $n % 2 == 0; });
    $squareEach = partial($map, function ($n) { return $n * $n; });
    $sum = partial($reduce, 0, function ($acc, $n) { return $acc + $n; });

    // それぞれの関数を合成し、偶数二乗和を計算する関数を作る
    $evenSquareSum = compose(
        $sum,
        $squareEach,
        $selectEven
    );

    $numList = range(1, $n);
    return $evenSquareSum($numList);
}
$ret = evenSquareSum(10);
var_dump($ret); // => 220

動いてそう。 これのカリー化バージョン作って比べてみると 部分適用との違いとか分かりやすいかも。

go の配列操作ライブラリを探す

go でそれなりに複雑なロジックを実装する時に欲しくなってくるのが配列操作ライブラリ。 何でも for でやるのが go way なのかもしれませんが、XXX.filter(...).map(...) とかやりたいですよね。 lodash みたいなのはないかと思って探してみたところ、一応いくつかあるみたいでした。

が、どれもリフレクションを使う必要があったり、コードを自動生成したりで、イマイチでした。 今のところ generics がないのでしょうがないかもしれないですが、 さらに探してみたところ、 go-linq というライブラリを見つけました。 LINQ は元々 C# などで使えたもので、それの Go 版みたいです。 これが type assersions を使う事なく使えるみたいなので試してみました。

package main

import (
    "fmt"

    linq "github.com/ahmetb/go-linq"
)

type MyNum struct {
    Num int
}

func main() {

    // MyNum のスライスを生成
    var nums []MyNum
    linq.Range(1, 5).
    SelectT(func (num int) MyNum {
        return MyNum{Num: num}
    }).
    ToSlice(&nums) // <- 変数に直接入れられる!!

    // いつもの偶数二乗和
    var result MyNum
    result = linq.From(nums).
    WhereT(func (myNum MyNum) bool { // <- 引数が interface{} でなくていい!!
        // 偶数のみ抽出
        return myNum.Num % 2 == 0
    }).
    SelectT(func (myNum MyNum) MyNum { // <- 戻り値も interface{} でなくていい!!
        // 二乗する
        num := myNum.Num
        return MyNum{Num: num * num}
    }).
    AggregateT(func (acc, myNum MyNum) MyNum {
        // 合計する
        return MyNum{Num: acc.Num + myNum.Num}
    }).(MyNum) // Aggregate の返り値は type assersion 必要みたい。おしい

    fmt.Printf("%#v\n", result) // -> main.MyNum{Num:20}
    fmt.Printf("%#v\n", result.Num) // -> 20
}

なかなかよさそうですね。

  • map -> Select
  • filter -> Where
  • reduce -> Aggregate

と名前はちょっと違うものの、当面これを使ってみます。

webpack で実行可能な単一ファイルを作る

node.js で cli ツールを作った時に 簡単に実行する方法を考えてみました。 nexe を使う方法もありますが、ファイルサイズが大きくなるので javascript のコードのみまとめる事を想定します。

方針としては

  • ライブラリも使いたいので webpack で単一ファイルにまとめる
  • まとまったファイルに shebang をつける
  • ファイルを path の通った所に置いて実行権限をつける

て感じです。 これを node.js で実現するには以下をやります。

  • webpack で単一ファイルにまとめる
  • webpack.BannerPlugin で shebang を追加する
  • ファイルを path の通った所に置いて実行権限をつける

実行するスクリプトを作成する

いつも通り node.js の package を作ります。

$ mkdir webpack-test && cd webpack-test
$ npm init -y
$ npm install --save lodash
$ mkdir src
$ vim src/index.js

ソースを src/index.js に書いたのは後で使う webpack のデフォルト設定に合わせるためです。 src/index.js の内容は webpack の Getting Started を 参考に、こんな感じで。

const _ = require('lodash');
const main = () => {
   const message = _.join(['hello', 'world'], ' ');
   console.log(message);
};
main();

試しに実行してみます。

$ node index.js
hello world

表示されました。

webpack で単一ファイルにまとめる

webpack をインストールして実行してみます。

$ npm install --save-dev webpack webpack-cli
$ npx webpack
...
$ node ./dist/main.js
hello world

./dist/main.js が webpack によって出力されたファイルです。 中身を見ると、lodash もこのファイルに入っている事が分かります。 これで 単一ファイルにはできましたが、 実行可能にするには shebang を追加する必要があります。

実行可能にする

shebang を追加するための webpack の設定をします。 以下のようにコマンドラインでやってもいいのですが、

$ echo '#!/usr/bin/env node' | cat - ./dist/main.js > ./dist/main

今回は webpack の BannerPlugin を使い、さらにファイルに実行権限を追加するところまで自動でやります。 webpack.config.js を作り、以下のようにします。

const webpack = require('webpack');

module.exports = {
    mode: 'production', // to minify
    target: 'node',
    plugins: [
        new webpack.BannerPlugin({
            banner: '#!/usr/bin/env node', // add shebang
            raw: true,
        }),
        function() {
            this.hooks.done.tap('chmod', () => {
                fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
            });
        },
    ],
};

もう一度 webpack を実行し、 直接実行できるか試してみます。

$ npx webpack
$ ./dist/main.js
hello world

実行できました。 あとはこのファイルを PATH の通った所に配置し、 ファイル名を適当に変えれば OK です。

補足: hook の書き方

chmod する所は最初は

function() {
    this.plugin('done', () => {
        fs.chmodSync(path.resolve(__dirname, 'dist', 'main.js'), '755');
    });
},

と書いていたんですが、

$ nps webpack
DeprecationWarning: Tapable.plugin is deprecated. Use new API on `.hooks` instead

のようなメッセージが出たので調べたところ、 最初の例の書き方に変わったみたいです。

バージョン

$ node -v
v10.13.0
$ npx webpack -v
4.26.1

node.js でライブラリを使わずに関数型する

node.js で関数型したい時には Ramda などの ライブラリを入れるのが一般的だと思いますが、 ちょっとしたツールを書く時には Ramda 入れるのも大げさに思える時があります。

で、よくよく調べてみると、map, filter, reduce はあるし、 部分適用も Function.prototype.bind() を使えばよさそう。

あとはカリー化と関数合成ができればいいんだけど、 関数合成については reduce を使えばいける事に気付いて こんな感じで定義してみました。

const pipe = (...fns) => val => fns.reduce((acc, fn) => fn(acc), val);
const compose = (...fns) => val => fns.reduceRight((acc, fn) => fn(acc), val);
const bind = (fn, ...args) => fn.bind(null, ...args);

こんな感じで使えます。

const add = (a, b) => a + b;
const mul = (a, b) => a * b;

const add1 = bind(add, 1);
const mul2 = bind(mul, 2);

let ret = 0;
ret = pipe(
    add1,
    mul2
)(2);
console.log(ret); // -> 6

ret = compose(
    add1,
    mul2
)(2);
console.log(ret); // -> 5

カリー化はいい方法が見つかってないですが、 必要な場合は手動でやってます。

const addCurried = a => b => a + b;
const add10 = addCurried(10);
ret = add10(1)
console.log(ret); // -> 11

cfn-lint で CloudFormation のテンプレートの検証をする

CloudFormation のテンプレートを作っている時、 いちいちコンソールにアップして確認していたら時間がかかってしまう。 そんな時に便利なのが cli でテンプレートファイルの チェックをしてくれるcfn-lint。 使い方も簡単で

$ cfn-lint validate ./cf-template.yaml

とするだけ。 こいつがすごいのがフォーマットだけでなく、 存在していないプロパティや関数までチェックしてくれる事。 なので typo が早期発見できて、開発効率がめちゃ上がります。 インストールも簡単で

$ npm install -g cfn-lint

でいけます。 ただし、ARN を設定する所にテンプレート内で定義したリソースを書いた場合 コンソールでやれば問題なく通るのに、 以下のようなエラーが出てしまいます。

Resource: Resources > AlbListener > Properties > LoadBalancerArn
Message: Expecting an ARN, got 'mock-ref-Alb'
Documentation: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-elasticloadbalancingv2-listener.html

無視すればよいのですが、ちょっと気になります。

shell で tap 関数を作ってみた

rubytap メソッドというものがあって、 メソッドチェーンしてる時のデバッグに便利なんですが、 shell でパイプライン使ってる時の デバッグにも使えそうなので、作ってみました。

ただし、処理を入れる事はできなくて 標準エラーに内容を表示するだけです。

#!/bin/bash

function tap() {
    local input
    input=$(cat -)

    echo "$input" | sed -e 's/^/> /' >&2
    echo "$input" >&1
}

使い方はこんな感じで 内容を表示させたい所に tap をはさみます。

$ echo "hoge\nfuga" | tap | sed -e 's/hoge/fuga/'
> hoge
> fuga
fuga
fuga

tap で出力した行には先頭に > をつけてみました。

Swagger を試してみた

そろそろ手書きで API の仕様を作るのやめたいと思って swagger を試してみました。

swagger は別に単一のソフトではなくて、 Swagger Spec という仕様で書かれた定義書を 元にドキュメントを書いたり、コードを生成したりできるものみたいです。

Swagger Editor

オンライン のがありますが、 docker 版もあるのでそちらでやりました。

$ docker pull swaggerapi/swagger-editor
$ docker run -d -p 80:8080 swaggerapi/swagger-editor

localhost にブラウザでアクセスすればよいです。 定義を書いたら右側にドキュメントが表示されて、 さらに上のメニューから、モックとかクライアントを 作ってくれます。

Swagger UI

定義ファイルを読み込んでドキュメントを生成してくれるツールです。 デフォルトでは Swagger Editor と同じデザインですが、 カスタマイズもできるもよう。

まとめ

最初の学習コストはあるけど、導入した方がいいと思いました。 この yaml を git に入れておけば仕様決めの段階から プルリクで議論できるし、CI ツールで マージ→仕様書更新まで自動化できると思います。

ただ、そのままでは yaml の分割に対応してないんですが、 いくつかツールがあるみたいなので、なんとかなりそうな気がします。