このスライドはヤパチーエイジアハチオウジ2016の「[WIP]あなたがエンタープライズファンクショナルPHPライブラリTeto\Functoolsを採用しなければならない11個の理由」の発表内容です。
関連記事
お前誰よ
- うさみけんた ぞぬえぐぜ/っどさん
- GitHub:
zonuexe
(Pakagistも同じ) - Twitter:
@tadsan
- GitHub:
- ピクシブ株式会社でpixivってサービスやってる
- 最近はPHPの静的解析がアツい
さて
Twitterにて
2016年5月25日
あまり有名ではないのでもしかしたらご存じないかもしれませんが、PHPには高速高機能なforeachってイテレーション機構が言語組み込みにあるので、わざわざarray_mapとか書かなくてもいいんですよ!!!!!
— うさみけんた (@tadsan) May 25, 2016
まじめなはなし、array_mapは名前の通り配列しかサポートしてくれないので本当に不必要でつらい。 https://t.co/lO7UCVlW9l
— うさみけんた (@tadsan) May 25, 2016
array_mapが活きるパターンもがんばって挙げればなくはなくて、ひとつは array_map('floatval', range(1, 5) 、もうひとつは array_map(null, range('a', 'e') ,range(1, 5)) 。以上終了でございます。
— うさみけんた (@tadsan) May 25, 2016
array_mapに渡していいのはcallableな値のみで、 array_map(function(){ ... とか書き始めたらそれは敗北フラグ。素直に foreach で書け!
— うさみけんた (@tadsan) May 25, 2016
僕ずっと自分のことをクロージャ厨だと思ってたけど動的スコープのEmacs Lisp最高! とか叫んだりPHPでarray_map(function()... って書いたら負けフラグだと主張したりするし、自分で認識してるほどそうではないのかもしれない。
— うさみけんた (@tadsan) May 25, 2016
みたいなことを、ゆっくり30分かけて話します。
三三 ヾ(〃><)ノ゙ ... λ
おことわり
この発表の内容で出てくる用語などは厳密なものではなく、現代的な解釈を反映したものではない場合があります。
つまり適当にWikipediaを流し読んで、難しそうな部分を都合良く読み飛ばしたような感じです。
おことわり
予防線を張っておきますが筆者は本発表で触れるいろんな概念について専門教育を受けたり体系的な研究に取り組んだことがあるわけではないので、こと数学的な概念への言及は疑ってかかってくださいね。「Haskellの機能をコピーした」ってレベルでは理解してるけどHaskellerじゃないよ。
アジェンダ
- 函数ってなんだっけ
- PHPのイテレータ
- Teto\Functoolsの紹介
函数とは何か
- 函数と関数の意味は同じ、特別な含意はない
- 中学校で $f(x) = 3x + 2$ みたいなのを習った
- PHPに翻訳すると次のように書ける
function f($x) { return 3 * $x + 2; }
echo "f(3) = ", f(3), PHP_EOL;
函数とは何か
- 算数でならった公式のようなものは、
プログラミング言語の函数で表現できる
/** 円の面積 (πr²) */
function circle_area($r) { return M_PI * $r ** 2; }
echo "半径5: 面積=", circle_area(5), PHP_EOL;
// 半径5: 面積=78.539816339745
函数とは何か
- 用語をひとつだけ覚えてください
- 「適用」 (apply)
- $f(a)$: 「$a$ に $f$ を適用する」
- PHPの 函数呼び出し みたいな感じ
- いちいち「函数呼び出しする」はめんどいから
今日のところは「適用する」と呼ぶ
PHPでの「函数」
- 函数(文):
'f'
function f($a){ ... }
- メソッド:
[$obj, 'f']
- クラス内
public function f($a){}
- クラス内
- 静的メソッド:
'Klass::f
/['Klass', 'f']
- クラス内
public function f($a){ ... }
- クラス内
- 函数式: ローカル変数に代入可能
$f = function($a) { ... };
PHPでの「函数」 = callable
全ての函数は変数代入して利用できる。
$f = 'f'; // 関数の f()
$f = [$obj, 'f']; // メソッド $obj->f()
$f = 'Klass::f'; // 静的メソッド Klass::f()
$f($v);
array_map($f, range(1, 10));
まとめてcallable
と呼ぶ (タイプヒント可能!)
集合
- 難しい概念はおいといて
- なんか物が集まったやつ
- PHPの配列でそれっぽく表現できる
$fruits = ["りんご", "みかん", "ばなな"];
- 集合に含まれるものを「要素」(element)と呼ぶ
- 「配列の中身」のことも同じように要素と呼ぶ
お約束
- ここでは「同じ引数をなら同じ値が返る」
函数は「同じ」だってことにします- PHPでの一般的な解釈ではありません
- 中身でどんな処理をしようが「同じ」です
- これを「函数の外延的等値性」と呼ぶらしい
「同じ函数」の比較
どう書いても結果は同じなので等値。
function pow3($n) { return $n ** 3; }
function pow3($n) { return $n * $n * $n; }
function pow3($n) {
$t = $n;
for ($i = 0; $i < (3 - 1); $i++) {
$t *= $n;
}
return $t;
}
余談
「外延的等値性」なんて用語を持ち出すのが妥当かどうかはさておき、概念はとても重要です。「チューニング」や「リファクタリング」とは、出力を変化させることなく(あるいは想定内に収め)実行効率やコードの読みやすさの改善を図る技法だからです。ユニットテストは入出力が期待通りであることを検証するための良い手法です。
さらに余談
これはほんと余談なのだけれど「外延(extension)」の反対語は「内包(intension)」です(内包と外延)。Pythonを知ってる型は「内包表記(List comprehension)」を書いたことがあるかもしれませんね。集合の外延的表記とは、配列リテラルのように[1, 2, 3]
と要素をひとつひとつ並べ上げる書きかたのことです。
map
map
- 集合っぽいものの要素に対して、
それぞれに函数を適用した集合にするのがmap - 集合論の用語では写像
map
function x2($n) { return $n * 2; }
orig | 1 | 2 | 3 | 5 | 7 | 13 |
---|---|---|---|---|---|---|
new | 2 | 4 | 6 | 10 | 14 | 26 |
「集合っぽいもの」を表にしてみる。
「集合」っぽいもの
- 配列
- 要素の重複を考慮する必要あり
- 配列のキー
- 重複はなし、ただし文字列と整数に限る
- ジェネレータ
- 割と最近(PHP 5.5, 2013年)に入った
どうするか
- PHP標準機能
-
foreach
でループを回す -
array_map
で配列要素に適用する
-
foreach
function x2($n) { return $n * 2; }
$orig = [1, 2, 3, 5, 7, 13];
$new = [];
foreach ($orig as $o) {
$new[] = x2($o);
}
外延的等値性の話をしたので立派にこれもmap
array_map
function x2($n) { return $n * 2; }
$orig = [1, 2, 3, 5, 7, 13];
$new = array_map('x2', $orig);
- ちっちゃい配列なら別にこれでいい
- 長大な配列で最終的に全ての要素を利用しない場合、無駄な処理が発生する
何が問題なのか
- 標準のmap函数は
array_map
しかない! -
foreach
で書くと一時変数が必要だし -
array_map
は配列にしか利用できない-
Traversable
クラス - ジェネレータ
-
-
function
とかいちいち入力するのはだるい
解決する手段
- イテレータライブラリを導入する
- Teto\Functoolsを導入する
イテレータライブラリ
-
nikic\iter
- 単純なイテレータじゃない機能も入ってる
- Underbar.php
- Ginq
これらのライブラリには処理を遅延評価(必要になるまで計算しない)機能が含まれている。
べつに自作してもいい
巨人の肩に乗った方が良いので推奨もしない
<?php /* tadsan@zonu.me | license WTFPL */
/**
* @param array|\Traversable $iter
* @param callable $callback
* @return \Generator
*/
function map($iter, callable $callback)
{
foreach ($iter as $k => $v) {
yield $k => $callback($v);
}
}
/**
* @param array|\Traversable $iter
* @param callable $callback
* @return \Generator
*/
function map_kv($iter, callable $callback)
{
foreach ($iter as $k => $v) {
yield $k => $callback($k, $v);
}
}
/**
* @param array|\Traversable $iter
* @return array
*/
function to_array($iter)
{
if (is_array($iter)) {
return $iter;
}
$ary = [];
foreach ($iter as $i => $v) {
$ary[$i] = $v;
}
return $ary;
}
/**
* @param array|\Traversable $iter
* @param int $n
* @return \Generator
*/
function take($iter, $n)
{
$i = 0;
foreach ($iter as $x) {
if ($n <= $i++) { return; }
yield $x;
}
}
イテレータだけでは解決しない問題
-
function
って打つのはやっぱりだるい!!
ここでやっと本来のタイトルの提案
あなたがエンタープライズファンクショナルPHPライブラリTeto\Functoolsを採用しなければならないn個の理由
Functoolsの機能
- 部分適用
- 演算子ラッパー
- ソート函数ラッパー
- 無名再帰
- メモ化
- 函数合成 / パイプ
Functoolsのもくろみ
mapを利用するときに全力でfunction
って書かないようにする!
インストール
composer require zonuexe/functools
Composerを読み込む
<?php
include_once __DIR__ . '/vendor/autoload.php';
基本
PHPスクリプトの先頭の方でクラスをインポートする。
<?php
use Teto\Functools as f;
部分適用
既存の函数に引数を当てはめた状態の函数を作る
implode(",", range(1, 10));
// "1, 2, 3, 4, 5, 6, 7, 8, 9, 10"
$comma = f::partial("implode", [", "]);
$comma(range(1, 10));
// "1, 2, 3, 4, 5, 6, 7, 8, 9, 10"
演算子ラッパー
2 + 3;
$add = f::op("+");
$add(2, 3); // (2 + 3) === 5
// 同時に部分適用もできる
$add_1 = f::op("+", [1]);
$add_1(4); // (1 + 4) === 5
言語構造ラッパー
include
やprint
は函数じゃないのでサポート。
array_map(f::op('include_once'), $files);
array_map(f::op('eval'), $codes);
↑ いろいろ危ないかもしれないので推奨はしない
言語構造ラッパー
// Before
$is_odd = function ($n) { return $n % 2 == 1; };
$odd_even = function ($n) use ($is_odd) {
return sprintf("%d: %s", $n, $is_odd($n) ? '奇数' : '偶数');
};
// After
$is_odd = f::compose(f::op('%', [1 => 2], 0), f::op('==', [1 => 1], 0));
$odd_even = f::op('if', [$is_odd, f::partial('sprintf', ['%d 奇数'], 1), f::partial('sprintf', ['%d 偶数'], 1)]);
array_map($odd_even, range(1, 10));
どっちが読みやすいかはご自身で判断をry
sortラッパー
配列ソート函数は仕様上map
できないので、
いい感じにラップしてる。
array_map(f::op('ksort'), $arrays);
array_map(f::op('sort', [1 => SORT_NUMERIC]), $arrays);
無名再帰 (fix)
Haskellの同名の函数をコピーした。
Haskell/不動点と再帰 - Wikibooks
$fib = f::fix(function ($fib) {
return function ($x) use ($fib) {
return ($x < 2) ? 1 :
$fib($x - 1) + $fib($x -2);
};
});
$fib(6); // 13
タプル (固定個の値の組)
任意のオブジェクトをキーにできる。
$teto = f::tuple(':name',"Teto Kasane",':age',31,':birth',"2008-04-01",':item',"Baguette");
$teto->pget(':name'); // "Teto Kasane"
$teto->pget(':age'); // 31
$teto->pget(':birth'); // "2008-04-01"
$teto->pget(':item'); // "Baguette"
内部構造はimmutableなセルによる連結リスト
カリー化
PHPでカリー化が適切な場面は皆無(断言)
メモ化
漸化式になるような単純な再帰函数を効率よくメモ化する
// simple fibonacci function. But, very very slow.
$fib1 = function ($n) use (&$fib1) {
return ($n < 2) ? $n : $fib1($n - 1) + $fib1($n - 2);
};
// simple fibonacci function too. very fast!
$fib2 = f::memoize(function ($n) use (&$fib2) {
return ($n < 2) ? $n : $fib2($n - 1) + $fib2($n - 2);
}, /*initialize=*/ [0, 1]);