PHP
ポエム

array_mapにありがとう、さよなら

More than 1 year has passed since last update.

このスライドはヤパチーエイジアハチオウジ2016の「[WIP]あなたがエンタープライズファンクショナルPHPライブラリTeto\Functoolsを採用しなければならない11個の理由」の発表内容です。


関連記事



お前誰よ


  • うさみけんた ぞぬえぐぜ/っどさん


    • GitHub: zonuexe (Pakagistも同じ)

    • Twitter: @tadsan



  • ピクシブ株式会社でpixivってサービスやってる

  • 最近はPHPの静的解析がアツい



さて



Twitterにて
2016年5月25日







みたいなことを、ゆっくり30分かけて話します。



三三 ヾ(〃><)ノ゙   ... λ



おことわり

この発表の内容で出てくる用語などは厳密なものではなく、現代的な解釈を反映したものではない場合があります。

つまり適当にWikipediaを流し読んで、難しそうな部分を都合良く読み飛ばしたような感じです。



おことわり

予防線を張っておきますが筆者は本発表で触れるいろんな概念について専門教育を受けたり体系的な研究に取り組んだことがあるわけではないので、こと数学的な概念への言及は疑ってかかってくださいね。「Haskellの機能をコピーした」ってレベルでは理解してるけどHaskellerじゃないよ。



アジェンダ


  1. 函数ってなんだっけ

  2. PHPのイテレータ

  3. 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とかいちいち入力するのはだるい



解決する手段


  1. イテレータライブラリを導入する

  2. Teto\Functoolsを導入する



イテレータライブラリ

これらのライブラリには処理を遅延評価(必要になるまで計算しない)機能が含まれている。



べつに自作してもいい

巨人の肩に乗った方が良いので推奨もしない

<?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



言語構造ラッパー

includeprintは函数じゃないのでサポート。

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]);



composer require zonuexe/functools



いますぐインストール