Functoolsを作った

  • 16
    いいね
  • 0
    コメント
この記事は最終更新日から1年以上が経過しています。

はじめに

Teto\Functoolszonuexe/php-functoolsは、数日前に「内部状態で頭がパンクして氏なないようにするには関数型言語がオススメ」との噂を真に受けて実装したライブラリだ。計画性のない人生でも、命は惜しい。

正直なところ僕には「関数型言語」が何を意味するのかよくわからないし、実際のところ単なる流行語に過ぎないと認識してるのだけれど、さしあたってPHPで「部分適用をしやすくしよう」を最初の目的に出発した。

このライブラリはイテレータ機能はカバーしないので、Underbar.phpとかGinqとか、好きなのを選べば良いと思ふ。この記事では特定のライブラリには依存せず、配列とPHP標準函数のarray_map, array_reduce, array_filterで説明する。

過去、いくつか関連のありそうな記事を書いたのだけれど、参考になるだろうか。 (たぶんならない)

それぞれのポエムに特に関連はない。

もうひとつはっきりしておくと、筆者はこのライブラリを日常業務で活用しようとする気は 全くない

関数型プログラミングの概念を知りたいと思ってこの記事を読むよりは、計算機プログラムの構造と解釈の読書会をやった方が遙かに有意義だ。原文(英語)、書籍版の日本語訳、有志による新訳、どれもWeb上で無料で読める。

なほ筆者は飽きっぽいので途中で積んでるもよう。

インストール

最近のPHPライブラリの導入にはComposerが便利だ。Rubyのgemなどと違って処理系と別にインストールしなければいけないが、便利なので入れておくと良い。

  1. phpコマンドがあることを確認
  2. Composer Getting Startedに従ってインストールする
    • この記事ではユーザーのホームディレクトリを前提に
  3. PATHCOMPOSER_HOME/vendor/binのパスを追加する
    • Unix系なら ~/.bashrcPATH=$HOME/.composer/vendor/bin:$PATH みたいな行を足せばいい
  4. (任意)PsySHをインストールする
    • Rubyのirbみたいなやつ
    • コマンドラインで composer.phar global require psy/psysh:@stable

これで準備はできたはずだ。PsySHはなくてもいいが、試行錯誤をしやすくなる。

次に、ライブラリをどこにインストールするかを決める。ComposerはRubyのBundlerのようにプロジェクト単位でインストールすることも、gemのように共通ライブラリとしてインストールすることもできる。

今回はコマンドラインで composer.phar global require zonuexe/functools:dev-master を実行して、$HOME/.composer/vendor以下にインストールされるようにしたい。

最新の状態にアップデートするときは composer.phar global update zonuexe/functools だ。

利用準備

.phpファイルを作って実行する場合は、ファイルにComposerのオートローダーを読み込む。

つまり、ファイルの先頭の方にこんな感じの行を書けばいい。

<?php
include_once '/home/yourname/.composer/vendor/autoload.php';
use Teto\Functools as f;

ComposerでインストールしたPsySHを使ってるなら、ファイルを読み込む必要はないが、 use Teto\Functools as f; と打ち込む。インストールに成功してようと失敗してようと、表示はfalseだ。

知っておくと良い言語機能

callable

函数のように呼べる値をPHPの用語ではコールバック/Callableと呼ぶ。この過剰なほどの柔軟さがPHPのいいところだ。

function hoge() { return "HoGe"; }

// 標準函数の名前の文字列
$imp  = "implode";
$imp(',', ["a", "b", "c"]); // "a,b,c"

// ユーザー定義函数の名前の文字列
$hoge = "hoge";
$hoge(); // "HoGe"

// クロージャ
$fuga = function(){ return "FuGa"; };
$fuga(); // "FuGa"

// インスタンスとメソッド名の文字列の配列
$timestamp = [new \DateTime, "getTimeStamp"];
$timestamp();

class MyClass
{
    public function __invoke() { return "MyClass __invoke!"; }
    public static function foo() { return "MyClass static foo!"; }
    public function bar() { return "MyClass bar!"; }
}

$my_instance = new \MyClass;

// __invoke() を実装したユーザー定義クラスのインスタンス
$my_instance(); // "MyClass __invoke!"

// インスタンスと静的メソッド名の配列

// クラス名と静的メソッド名の配列
$my_class_foo = ["MyClass", "foo"];
$my_class_foo(); // "MyClass static foo!"

// (特殊)クラス名の文字列とメソッド名を結合したもの
$my_str_foo = "MyClass::foo";
call_user_func($my_str_foo); // "MyClass static foo!"

Teto\Functoolsの実装には、だいたい使ってる気がする。

配列と参照渡し

PHPで実用的なコードを書こうとすると決して避けて通れないが、この記事では深く解説することはしない。

ただ、Rubyなどの言語と違った動きをするので、気に留めておいていただきたい。(PHPとRubyのどちらが「正しい」とか「直感的だ」と言った水掛けの議論には何の意味もなく、言語設計上の選択に過ぎない)

以下のようなコードでは、RubyとPHPではまったく同じように書いても違った挙動になる。

set_key_val = ->(hash, key, value){
  hash[key] = value
  return hash
}

h1 = {"a" => "AAA"}
h2 = set_key_val.call(h1, "b", "BBB")

p h1 #=> {"a"=>"AAA", "b"=>"BBB"}
p h2 #=> {"a"=>"AAA", "b"=>"BBB"}
$set_key_val = function ($hash, $key, $value) {
    $hash[$key] = $value;
    return $hash;
};

$h1 = ["a" => "AAA"];
$h2 = $set_key_val($h1, "b", "BBB");

var_export($h1); // array('a' => 'AAA')
var_export($h2); // array('a' => 'AAA', 'b' => 'BBB')

後述するが、この制約によりPHPの配列ソート函数群は、RubyのArray#sort!(破壊的ソート)に相当するものしかなく、標準では配列を破壊しないバージョンは「ない」。

機能紹介

アリティを調べる

アリティ(arity)とは「函数が要求する引数の個数」のことだ。

f::arity("implode"); //=> 2
f::arity(function($a, $b, $c){}); //=> 3
f::arity(["DateTimeImmutable", "createFromFormat"]); //=> 3
f::arity("fuga");// 存在しない函数
PHP error:  Argument 1 passed to Teto\Functools::arity() must be callable, string given, called in ...

存在しない函数を指定した場合には例外ではなくエラーを送出するので、そのことだけ気に留めておいてほしい。これはタイプヒンティングの機能で、PHP側が引数を評価する段階で検出する。

部分適用

f::partial(calable $callback, array $arguments, [$pos])は、渡されたcallableオブジェクトを部分適用したcallableオブジェクトを返す。

$callableは部分適用したい対象のcallableオブジェクトを、$argumentsは部分適用する引数を、$posは省略可能な変数で、その引数が渡されたときに、引数リストのどの位置に割り当てるかを指定する。

返されるオブジェクトの実体はPartialCallableクラスのインスタンスだ。partial()メソッドを呼ぶことで、更に部分適用をすることができる。当然、オブジェクトの内部状態を変更するのではなく新しいインスタンスオブジェクトを作って返すので安心して欲しい。

$input = <<<EOS
Subject: Hoge : Fuga
Date: 2008-04-01
Option: foo bar buz
EOS;

$split_colon1 = function ($s) { return explode(": ", $s, 2); };
$split_colon2 = f::partial('explode', [0 => ": ", 2 => 2], 1);
$split2 = f::partial('explode', [2 => 2], 1);
$split_colon3 = $split2->partial([0 => ": "])

$split_colon1("Subject: Hoge : Fuga"); // => ["Subject", "Hoge : Fuga"]

$result = array_reduce(
    array_map($split_colon2, explode("\n", $input)),
    function($acc, $a) { $acc[$a[0]] = $a[1]; return $acc; },
    []
);
// [
//     "Subject" => "Hoge : Fuga",
//     "Date"    => "2008-04-01",
//     "Option"  => "foo bar buz"
// ]

$inputは特定のフォーマットではないが、なにかのヘッダーらしきものだ。

$split_colon1$split_colon2は、おなじ機能を持つ。

function($acc, $a) { $acc[$a[0]] = $a[1]; return $acc; }とかわざわざ何度も書くのはだるいので、後で説明するべんりユーティリティに収録してある。

実行時に引数を渡さない

// $posに -1 を指定
$sleep_1 = f::partial('sleep', [1], -1);
array_map($sleep_1, range(0, 10));

この場合、実行される処理は sleep(1, $n) のように要素が渡されるわけではなく、実行時に渡された引数は捨てられ、sleep(1)が10回実行される。


ここまでさらっと「部分適用」などと言ってるが、PHPの文脈では特に定まった用法ではないので、その意味は自明ではない。

Haskell(やOCaml, F#)のような言語においてどのような意味を持つかについては、やさしい Haskell 入門 (Version 98): 3 関数にまとまってるので、興味があれば読んでおいて欲しい。

べんりコールバックを取得/実行

Operator.phpにべんり機能とか標準機能のラッパーをたくさん用意した。

f::apply('+', [1, 2]); // 1 + 2 => 3
array_map(f::op('-@'), range(1, 10);
$half = f::op('/', [1 => 2], 0);
$half(10); // 5

f::op()f::apply()の第一引数は共通で、演算子を表現するシンボル(文字列)を渡す。シンボルとは +>> などの演算子そのものや、mod(modulus, 剰余)、ge(Greater than or equal, 左辺が右辺以上)などの短いキーワードなどだ。

f::op()の第二引数以降を指定したときは、内部でf::partial()に渡して部分適用される。

f::apply()は、その場で実行する。高階関数として渡したいときなどは f::op() ってことで覚えておいてほしい。

どんな機能があるのかは Operator.php を見て欲しいが、特別なことをしてるわけではなく、ひたすら賽の河原で石を積むようにPHPのいろんな機能をラップしまくってるだけだ。

演算子ラッパー

2 + 3のような演算子式を部分適用可能にするために、メソッドでラップしたものだ。Pythonのoperatorモジュールに近い。

array_map(f::op('*'), range(0, 9), range(101, 110));
// [0, 102, 206, 312, 420, 530, 642, 756, 872, 990]

$plus_1 = f::op('+', [1]);
array_map($plus_1, range(50, 59));
// [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]

単にPHPの演算子式をラップしたに過ぎないため、/などは返される型が値によって一定ではないことに注意されたい。

$half = f::op('/', [1 => 2], 0);
$half_type = f::compose($half, 'gettype');

$half(100);      // 50
$half_type(100); // integer
$half(1);        // 0.5
$half_type(1);   // double

現在は二項演算子は二つの演算子しか受け付けないが、Lispの(+ 1 2 3)のように可変長引数を受け付けることも検討はしてる (未実装)

範囲演算子

$input = [1, 4, 1.2, 1.9, 2, 2.1, 1.4];
array_filter($input, f::op('< @ <', [1, 2]));
// [2 => 1.2, 3 => 1.9, 6 => 1.4]

array_filter($input, f::op('<= @ <', [1, 2]));
// [0 => 1, 2 => 1.2, 3 => 1.9, 6 => 1.4]

Pythonの範囲演算子に影響を受けたが、これは三項限定だ。

ソート函数ラッパー

「配列と参照渡し」に書いたが、PHPの標準函数の配列のソート函数群は「変数の参照」を要求する。

そのため、変数に代入してない値を直接ソートしたり、mapに渡して配列の配列を全てソートする、といったことが標準ではできない

$arrays = [
    [1=> "b", 3 => "D", 0 => "A", 2 => "c"],
    [1=> "b", 3 => "D", 0 => "A", 2 => "c"]
];

/* 動かないパターン */
ksort([1=> "b", 3 => "D", 0 => "A", 2 => "c"]);
// PHP Fatal error:  Only variables can be passed by reference in

/* エラーにはならないけど期待する結果が得られない */
array_map('ksort', $arrays);
// [true, true]

/* 安全なバージョン */
f::apply('ksort', [[1=> "b", 3 => "D", 0 => "A", 2 => "c"]]);
// ["A", "b", "c", "D"]

/* mapにも渡せる */
array_map(f::op('ksort'), $arrays);
// [["A", "b", "c", "D"], ["A", "b", "c", "D"]]

この問題は、以上のように標準函数に薄いラッパーを被せてやることで解決できる。

べんりユーティリティ

上で紹介したヘッダーをパースする処理に使った、[["key1", "value1"], ["key2", "value2"]]["key1" => "value1", "key2" => "value2"]のような連想配列に変換する処理は頻出なので、その場その場で実装するの億劫だ。

array_reducef::op('tup_to_kv')を渡してやればいい。

$input = <<<EOS
Subject: Hoge : Fuga
Date: 2008-04-01
Option: foo bar buz
EOS;

$result = array_reduce(
    array_map(f::partial('explode', [0 => ": ", 2 => 2], 1), explode("\n", $input)),
    f::op('tup_to_kv'),
    []
);

SKIコンビネータ

これを実装したのは完全に趣味だ。Kコンビネータは最初からカリー化しても良かったのだが、他の函数との調和を考へて、非カリー函数で実装したものをIコンビネータの実行時にf::curry()でラップした。

SKK=Iではないシンプルなバージョンの恒等函数 f::op('id') もちゃんと用意してあるので、普通はそっちを使った方がいい。

Currying

強調しておくが、「カリー化」として知られるこの機能がPHPはおいて大した意味を持たないし、役に立つ場面は非常に限定的だろう。筆者の想定するケースではf::partial()でおよそ事足りる。

この機能について多くは語らないが、PHPはML族の言語ではないので擬似的な再現に過ぎないこと、可変長引数との相性が悪い(アリティを得られない)こと、そして、SKK=Iを綺麗に実装するためだけに用意されたことだけは記しておく。

函数合成

説明がめんどくさいので、使ってみて察してください。

array_map(f::compose(f::op('*', [5]), f::op('/', [1 => 3], 0), 'ceil', 'intval'), range(1, 10))
// => [2, 4, 5, 7, 9, 10, 12, 14, 15, 17]

タプル

タプルと言ってるが、内部実装はコンスセルっぽいクラスを使った連結リストで、ややこしいことに、f::tuple()の動きはLispの(list ...)と同じだ。ただ、破壊的操作を用意してないのでイミュータブルと見做せる。

$t = f::tuple("a", "b", "c");

// nth
$t[0]; // "a"
$t[1]; // "b"
$t[2]; // "c"
$t[3]; // null

// plist
$teto = f::tuple(":name", "Teto Kasane",  ":age", 31, ":birthday", "2008-04-01", ":item", "Baguette");
$teto->pget(":name");     // "Teto Kasane"
$teto->pget(":age");      // 31
$teto->pget(":birthday"); // "2008-04-01"
$teto->pget(":item");     // "Baguette"

連想リストの機能もプロパティリストの機能も実装がおてがるにできて、シンプルなデータ構造の柔軟さってすごいなと思った。(小学生並の感想)

ミュータブルで複雑なPHPの配列をタプルの代替にするのは嫌だな、と思って実装を始めたけど、任意のオブジェクトをキーにできるのは意外とべんりかもしれない。

f::cons()も用意したので、既存のタプルを破壊せずに情報を足していくことも簡単。

無名再帰(不動点コンビネータ)

array_map(
    f::fix(function ($fib) {
        return function ($x) use ($fib) {
            return ($x < 2) ? 1 : $fib($x - 1) + $fib($x -2);
        };
    }),
    range(1, 10)
);
// [1, 2, 3, 5, 8, 13, 21, 34, 55, 89]

一時変数に代入しなくても再帰できる函数を作れる。PHPでこれができて一体何が嬉しいんだって? さあ…

まとめ

最初は部分適用だけがしたくて、しかも完全なお遊びだったのにFunctional toolboxって名前を付けちゃってどうしよう… みたいな感じだったけど、それなりの数が揃ってきちゃった。

みなさんも「ぼくのかんがえたさいきょうのかんすうがたらいぶらり」を作ってみてくださいね。