18
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Declined】PHPのあらゆる関数を部分適用できるようにするRFC

Last updated at Posted at 2021-05-16

正直なところ、カリー化や部分適用の有用性が私にはわからないんですよ。

	// 普通の関数
	function foo1(int $a, int $b):int{
		return $a + $b;
	}
	echo foo1(10, 5);

	// カリー化
	function foo2(int $a):Closure{
		return fn (int $b):int => $a + $b;
	}
	echo foo2(10)(5);

	// 部分適用
	$foo10 = foo2(10);
	echo $foo10(5);

読みにくくなっとるだけやん?

関数の再利用とか言うけどそんなに再利用するか?

まあ私の妄言はどうでもいいとして、なんかPHP本体であらゆる関数を部分適用できるようにするというRFCが提出されていました。
以下はPartial Function Applicationの紹介です。

Partial Function Application

Introduction

関数の部分適用とは、関数呼び出しの引数の一部だけを固定しておいて、残りは後から適用するというプロセスのことです。

このテクニックには、ふたつの利点があります。

開発者は、引数が利用可能になった時点で、その引数を埋めることができます。

有用そうな単純な例として、たとえば配列関数などが挙げられます。

	$result = array_map(do_stuff(?), $arr);

Proposal

このRFCは、新たなプレースホルダ?による部分適用をサポートします。
任意の関数、メソッド、その他callableな対象について、ひとつ以上の引数を?で置き換えることができます。
その場合、エンジンは引数をその場で関数に適用するのではなく、引数を値として取り込むClosureオブジェクトを生成して返します。
そのClosureオブジェクトの引数からは、既に与えられた引数は取り除かれ、それ以外は元の関数のシグネチャと一致します。

このClosureは、後から残りの引数を使って呼び出すことができます。

function whole($one, $two) {
    /* ... */
}

$partial = whole(?, 2);

$partialは以下の関数のようにふるまいます。

function($one) {
   /* ... */
}

論理的には以下と同じになります。

$partial = fn($one) => whole($one, 2);

Closureを呼んだときに、元々の引数とClosureの引数を合わせて元々の関数が呼び出されます。
Closureの引数は順に埋められ、余っていれば後ろに回されます。

function whole($one, $two) { /* ... */ }
 
// whole(1, 2, 3)と同じ
$result = whole(?, 2)(1, 3);

Types

型宣言、引数名は保持されます。

function f(int $x, int $y): int {}

$partial = f(?, 42);

これは以下と同じです。

$partial = function(int $x): int {
    return f($x, 42);
};

Variables/References

引数のリファレンスも維持されます。

function f($value, &$ref) {}

$array = ['arg' => 0];

$f = f(?, $array['arg']);

は以下と同じです。

$ref = &$array['arg'];
$f = function($value) use (&$ref) {
    return f($value, $ref);
};

Optional Parameters

省略可能引数とデフォルト値も引き継ぎます。

function f($a = 0, $b = 1) {}

$f = f(?, 2);
```php

は以下と同じです。

```php
$f = function($a = 0) {
    return f($a, 2);
};

func_num_args et al.

func_num_argsは、関数が直接呼び出されたときと同じように動作します。

function f($a = 0, $b = 0, $c = 3, $d = 4) {
    echo func_num_args() . PHP_EOL;
 
    var_dump(
        $a, 
        $b, 
        $c, 
        $d);
}
 
f(1, 2);
 
$f = f(?);
 
$f(1, 2);

結果はこうなります。

2
int(1)
int(2)
int(3)
int(4)
2
int(1)
int(2)
int(3)
int(4)

Methods

既に部分適用された関数や、メソッドなど、あらゆるcallableが部分適用の対象になります。
以下は正しい構文です。

class Action {
    public function volume(int $x, int $y, int $z): int {
        return $x * $y * $z;
    }
 
    static public function vol(int $x, int $y, int $z): int {
        return $x * $y * $z;
    }
}

$p1 = Action::vol(3, 5, ?);
print $p1(9); // 135

$a = new Action();
$p2 = $a->volume(3, 5, ?);
print $p2(9); // 135

Extra Arguments

PHPの関数には、シグネチャに記載されているより多くの引数を渡すことができます。
これは部分適用も同様です。

function f($a, $b) {
    print_r(func_get_args());
}
 
$f = f(?, 2);

これは実のところ、

$f = function($a, ...$extraArgs) {
    return f($a, 2, ...$extraArgs);
};

と同じようなものです。
従って、引数が多い場合の結果は

	$f(1, 3, 4); // Array( [0] => 1, [1] => 2, [2] => 3,  [3] => 4)

のようになります。

Trailing Placeholders

部分適用には、少なくともひとつの引数プレースホルダが必要です。
それ以降の引数はあってもなくてもかまいません。
これは引数の多い関数の部分適用に役立ちます。

function f($m, $n, $o, $x = 0, $y = 0, $z = 0) {}

$f = f(1, ?);

これは以下と同じです。

$f = function($n, $o, $x = 0, $y = 0, $z = 0) {
    return f(1, $n, $o, $x, $y, $z);
};

この副次効果として、?ひとつだけを渡すと変化のない部分適用ができます。

class Foo {
  public function bar($a, $b, $c, $d, $e, $f, $g, $h): string { ... }
}

$f = new Foo();
$p = $f->bar(?);

$pの引数は8個のままであり、$p(1, 2, 3, 4, 5, 6, 7, 8)$foo->bar(1, 2, 3, 4, 5, 6, 7, 8)は全く同じです。
メソッドをcallableとして取り扱いたいときに有用かもしれません。

Extra Trailing Placeholders

関数シグネチャを超えた引数にプレースホルダを与えてもエラーにならず、関数に影響を与えることもありません。
結果として遅延評価に使えます。

$n = 1000000;

$log10ofn = log10($n, ?);

は以下と同じです。

$log10ofn = function() use ($n) {
    return log10($n);
}

Variadic Functions

可変長引数の関数もシグネチャを保持します。
可変長引数はオプションであり、デフォルト値はありません。

function f(...$args) {
    print_r($args);
}
 
$f = f(?, 2, ?, 4, ?, 6);
$f(1, 3);

結果はこうなります。

Array
(
    [0] => 1
    [1] => 2
    [2] => 3
    [3] => 4
    [4] => 6
)

Named arguments

名前付き引数も使用可能で、引数名は部分関数に引き継がれます。
ただし、引数の順番は、部分関数の引数の順番ではなく元の関数の順番になります。

以下の例では、関数messageの部分適用で名前付き引数の順番を変えていますが、得られる部分関数$pの引数は、元の関数の順番のままです。
もちろん部分関数を名前付き引数で呼び出すこともできます。

function message(string $salutation, string $name, string $stmt): string {
    return "$salutation, $name. $stmt" . PHP_EOL;
}

$p = message(stmt: ?, name: ?, salutation: "Hello");
// ↓は同じ
print $p('World', 'How are you.');
print $p(stmt: 'How are you.', name: 'World');

Evaluation order

既存のラムダ関数との違いは、引数式が事前評価されることです。

function getArg() {
  print __FUNCTION__ . PHP_EOL;
  return 'hi';
}

function speak(string $who, string $msg) {
  printf("%s: %s\n", $who, $msg);
}

$arrow = fn($who) => speak($who, getArg());
print "Mark\n";
$arrow('Larry');
/*
	Mark
	getArg
	Larry: hi
*/

$partial = speak(?, getArg());
print "Mark\n";
$partial('Larry');
/*
	getArg
	Mark
	Larry: hi
*/

部分適用の場合、PHPエンジンは引数を全て評価した後でプレースホルダ引数が存在することに気が付くためです。
ラムダ関数では、PHPエンジンは先にClosureを生成するため、中身は実際に呼ばれたときに初めて評価されます。

Constructors

コンストラクタの生成処理は2段階に分かれています。
まずオブジェクトを作成し、その後コンストラクタを呼び出してオブジェクトを初期化しています。
部分適用を素朴に実装すると、この間に部分適用が適用されることになります。
すなわち、複数のオブジェクトを生成するのではなく、ひとつのオブジェクトにコンストラクタが複数回適用されることになります。

これはユーザにとって想定外の動作であり、望ましくありません。
そのため、コンストラクタでは特別な処理を行っており、毎回新しいオブジェクトを生成するようになっています。

たとえば以下のようにすると、想定どおりに4つのオブジェクトが作成されます。

class Point {
  public function __construct(private int $x, private int $y) {}
}
 
$data = [
  [1, 2],
  [4, 6],
  [3, 9],
  [7, 4],
];
 
// 動く
$points = array_map(new Point(?, ?), $data);

Optimizations

部分適用はClosureインスタンスとして実装されますが、部分適用が繰り返された場合には同じ実装を排除する最適化が行われます。
そのため、最終的にスタックフレームは1つしか消費されません。

function test(int $a, int $b, int $c, int $d, int $e) { ... }

$one = test(1, ?)(2, ?)(3, ?)(4, ?);
$two = test(1, 2, 3, 4, ?);
$three = fn(int $e) => test(1, 2, 3, 4, $e);

// 全てtest(1, 2, 3, 4, 5);が呼ばれる
$one(5);
$two(5);
$three(5);

Reflection

部分適用は既存のClosureオブジェクトを使用して実装されるため、追加のリフレクションはありません。
部分適用で生成されたClosureは、手動で作成した同等のClosureと同じものです。

Comparison to other languages

部分適用はコンピュータサイエンス分野ではよく見られるパターンです。
しかし、専用の構文を持つ主要言語はほとんど存在せず、『自分でアロー関数とかで実装しろ』とユーザ空間での実装に頼っています。

部分適用をネイティブサポートしている言語はHaskellやOCamlのような高度な関数型言語で、これらの言語では全ての関数は自動的に単一引数の関数にカリー化されます。
必要数よりも少ない引数で関数を呼び出すと、自動的に部分適用されたうえで残りの引数を受け取る関数が返されます。
この方法の限界は、引数を必ず左から埋める必要があり、一番右の引数だけを先に埋めるといったことができないことです。

例外としてRakuには右の引数を埋める方法があります。

このRFCで説明した機能は、現在主要な言語の中でも、最も堅固で強力な部分適用の構文をPHPに与えることになります。
これは率直に言って、とても素晴らしいことです。

参考:https://rosettacode.org/wiki/Partial_function_application

Syntax choices

プレースホルダに?を選んだのは、曖昧さが存在せず、実装が容易であることが主な理由です。
大きな問題が見当たらないかぎり、部分適用の構文は?です。

Related RFCs

このRFCは独立ですが、現在進行中の一部のRFCとシナジーがあります。

パイプ演算子のRFCでは新たな演算子|>が提案されていますが、PHPのcallableの構文が貧弱であることが障害となっていました。
このRFCを組み合わせることで問題がほぼ解決し、以下のような構文が可能となります。

$result = $var
	|> step_one(?)
	|> step_two(?, 'config')
	|> $obj->stepThree('param', ?);

パイプ演算子の初期バージョンのRFCでは、部分適用と同等の機能がパイプ演算子に直接組み込まれていました。
このふたつを分離することで、部分適用を単独で利用できることに加え、パイプ演算子においての利便性も確保できるようになります。

Backward Incompatible Changes

下位互換性のない変更はありません。

Proposed PHP Version(s)

次のメジャーバージョンかマイナーバージョン。

感想

このRFCで提唱されている部分適用の大きな特徴として、左端だけでなく任意の引数を部分適用できることが挙げられます。
これは他の言語にはほとんど存在しない、強力な機能だそうです。
これを使って何か面白いことができるかもしれませんね。

strictなin_array
$in_array_strict = in_array(?, ?, true);

$in_array_strict(1, [1]); // true
$in_array_strict(1, ['1']); // false

まあ今でも$in_array_strict = fn ($needle, $heystack) => in_array($needle, $heystack, true);とか書けるんですが、長さと見やすさが圧倒的に違います。

このRFCは、まだ先日提出されたばかりです。
従って今後どう進むかはわからないのですが、メーリングリストでの評価も悪くないようです。
MLでは引数の数などの曖昧だったりな部分とかが議論されているので、もしかしたら今後文法の変更などがあるかもしれません。

なお、細部まで詰めきっているわけではないみたいですが、既に実装が存在しています。
またAuthorのLarry GarfieldJoe WatkinsはPHP srcでよく見かける人物で信頼性もあります。
そんなわけでこのRFCは、五分五分以上の割合で受理されるんじゃないかなと思っています。

ただ個人的には、このRFC単独での利点はあんまりなさそうかなと感じました。
部分適用が真骨頂を発揮するのは、パイプ演算子が導入されてからだと思います。
この2つのRFCだけで、これまでとは別次元のPHP構文が書けるようになりそうですね。

18
12
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
18
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?