Edited at

部分適用ってなんだ

More than 3 years have passed since last update.


はじめに

この記事はポエムです。「ポエムである」とは、形式的定義に依らず、頭の中にあるふわっとした概念をだらだらとdumpした文章に過ぎないことを意味します。

カレーはとてもスパイシーで魅惑的な料理ですが、PHPでは特に覚えておく必要のある単語ではないので忘れてください。OCamlとかHaskellに興味が湧いたらまたきてね。


部分適用(partial application)とは

いくつかの引数が必要な函数があったとします。ここでは例として台形の面積を求めます。

台形の面積公式は $\frac{(上底 + 下底) \times 高さ}{2}$ だと小学校で習ったおぼえがあるので、PHPでは以下のように書けます。

<?php

/**
* @param int|float $height 高さ
* @param int|float $b1 上底
* @param int|float $b2 下底
* @return float 台形の面積
*/

function area_trapezoid($height, $b1, $b2)
{
return (float)((($b1 + $b2) * $height) / 2);
}

area_trapezoid(3, 5, 4); // 13.5

じゃあ次に上底を3、下底を5に固定して、高さだけを変更したいとします。

area_trapezoid(なにか, 3, 5);

こんなふうに、決まってる引数を穴埋めするのが部分適用の基本的な概念です。

なにかって何だよ。どうやって実現すりゃいいんだよ、ってのはPHPでもいくつもやりかたがあるので紹介します。


functionを定義する

function area_trapezoid_3_5($height)

{
return area_trapezoid($height, 3, 5);
}

foreach (range(1, 10) as $n) {
var_dump(area_trapezoid_3_5($n));
}

とてもわかりやすいやりかたです。しかし、「30と50に固定したい」ときには area_trapezoid_30_50() をいちいち定義しなければいけなくなります。

また、函数定義はグローバルに影響するので、あまりやりたくありません。


classを定義する

final class Trapezoid

{
/** @var int|float */
private $b1;
/** @var int|float */
private $b2;

public function __construct($b1, $b2)
{
$this->b1 = $b1;
$this->b2 = $b2;
}

public function area($height)
{
return area_trapezoid($height, $this->b1, $this->b2);
}
}

$trapezoid_3_5 = new Trapezoid(3, 5);

foreach (range(1, 10) as $n) {
var_dump($trapezoid_3_5->area($n));
}

このアプローチのいいところは、$t2 = new Trapezoid(30, 50)のように別のインスタンスを作れば、固定する値を簡単に変更できるところです。

しかし、面積を求めたいがためにクラスを定義するのは大がかり過ぎる感じもします。

それに、「上底と高さ」を固定したいと思ったときはクラスの設計を変更したり、別のメソッドを定義しなければなりません。


クロージャ

$trapezoid_fix_base = function ($b1, $b2) {

return function ($height) use ($b1, $b2) {
return area_trapezoid($height, $b1, $b2);
};
};

$trapezoid_area_3_5 = $trapezoid_fix_base(3, 5);

foreach (range(1, 10) as $n) {
var_dump($trapezoid_area_3_5($n));
}

この方法の良いところは、単なる変数なので、(適切にメソッド内などで利用すれば)影響範囲をローカルにできることです。また、クラスを定義したときと同じように、インスタンスを生成するのと似た感覚で利用することができます。

クラスを定義するときと比較したデメリットとしては、同じパラメータを使って面積ではなく周の長さを求めたい、と思ったときに値を再利用しにくいことです。 (工夫をすればできなくはないんだけど、うーん…)

上の例は柔軟にしたいがためにやりすぎた感じもあるので、下のようにも書けます。

$b1 = 3;

$b2 = 5;
$trapezoid_area = function ($height) use ($b1, $b2) {
return area_trapezoid($height, $b1, $b2);
};

foreach (range(1, 10) as $n) {
var_dump($trapezoid_area($n));
}

$trapezoid_area_3_5 = function ($height) {

return area_trapezoid($height, 3, 5);
};

foreach (range(1, 10) as $n) {
var_dump($trapezoid_area_3_5($n));
}


素朴な解答

最初の題意を満たすなら、奇を衒ったりしないで、これが一番素直でいいんじゃないかな…

foreach (range(1, 10) as $n) {

var_dump(area_trapezoid($n, 3, 5));
}

大正義。


配列が欲しい

上の問題ではforeachでループを廻してvar_dump()で出力してますが、配列が欲しいとします。

$a = [];

foreach (range(1, 10) as $n) {
$a[] = area_trapezoid($n, 3, 5);
}

$result = $a;

はい、これで完成です。よかった!


今回のまとめ

素直にforeachすればいいし、array_mapとか特別な部分適用とか、PHPでは別に必要なかった。