はじめに
PHP8から名前付き引数が使用可能になりました。
名前付き引数とは、引数を渡す際にパラメータ名を指定することで、関数のパラメータが定義された順番に関係なく引数を渡すことができる構文です。
名前付き引数はコードの可読性を高めますが、パラメータ名を列挙する必要があるため、タイピングが面倒という欠点もあります。名前付き引数の利点と欠点を考えることで、状況に応じた適切なコードの書き方を学んでいきましょう。
本論
例のコード
$fn = function ($a, $b, $c) {};
$fn(c:1, a:2, b:3);
という関数がある場合、パラメータの順番に従って引数を渡すのではなく、パラメータ名を指定することで、パラメータの順序と引数の順序を異なる形で渡すことができる機能です。
PHPで関数パラメータの特徴
名前付き引数の使用について考える前に、PHPの関数パラメータの特徴についてもう少し考えてみましょう。
可変引数の問題
PHPでは、データセットを関数やメソッドに渡したい場合、配列型のパラメータを使うことが一般的です。しかし、配列パラメータの場合、配列内部のデータ構造が分かりにくいため、dockblock genericやarray shapeなどのドックブロックを追加するのが望ましいです。
個人的には、ドックブロックはボイラープレートが多くなりがちなので、複数の配列を渡したり、大量のデータを関数に渡すようなケースでなければ、多少のパフォーマンス低下があっても可変パラメータを使うコーディングスタイルを好みます。
可変パラメータを使えば、 fn(...['a', 'b', 'c'])
のように配列を展開して渡すことも、fn('a', 'b', 'c')
のように単純に列挙して渡すこともでき、構文的に簡潔でありながら、実行時の型チェックも活用できます。
可変パラメータは、同じ型のデータで構成されたデータセットを渡す場合に便利ですが、関数を定義する際にはパラメータの最後に書く必要があり、1つの関数につき1つの可変引数しか使えないという制約があります。
デフォルト値を持つパラメータの位置の問題
function ($param1, $paramWithDefault = 100, ...$variadicParams);
上記のコードは可能ですが、次の2つのコードは不可能です。
function ($paramWithDefault = 100, $param1, ...$variadicParams);
function (...$variadicParams, $paramWithDefault = 100, $param1);
その理由は、パラメータにデフォルト値が設定されている場合、デフォルト値のないパラメータをその後に配置することはできないというルールがあるためです。
どんなときに名前付き引数を使うと良いか?
-
関数を使用する際に、引数の渡す順番をパラメータの定義順と異なる形にしたいときに使います。
-
コードを読む際にパラメータ名を明示することで、関数の定義部分を確認しなくてもどんな引数が渡されているかを分かりやすくしたいときに使います。
-
デフォルト値が設定されているパラメータの引数の指定を省略したいときに使います。
常に名前付き引数を使うべきではない理由
function getUserById(int $id): User {}
上記の関数シグネチャの場合、関数名に byId が含まれているため、id を渡すことが直感的に分かります。そのため、わざわざ追加情報である名前付き引数を使う必要はありません。
function biggerThan(int $a, int $b): bool {}
上記の関数シグネチャの場合、英語の「A is bigger than B」という構文を思い起こさせるため、第一引数が第二引数より大きいかを判定する関数であることが分かります。もし逆に、第二引数より第一引数が大きいかを判定するのではなく、第一引数が第二引数より大きいかを確認する関数でなければ、直感的に分かりやすい関数にするようアドバイスするでしょう。
function subtract(int $a, int $b): int {}
上記の関数シグネチャの場合、第一引数から第二引数を引く関数だと理解できます。もし逆に、第二引数から第一引数を引く関数であったなら、直感的にわかりやすい関数にするようコードレビューで指摘するでしょう。
分かること
このように、関数名だけでもその役割が明確に分かる場合には、わざわざ追加情報として名前付き引数を使う必要はありません。
名前付き引数を使うべき場合
次のようなシグネチャを見ましょう。
function isUpdatedBy(int $userId, bool $excluded = false): bool
isUpdatedBy
を使用する場合、isUpdatedBy(userId: $id)
または isUpdatedBy(userId: $id, excluded: true)
のように使えます。excluded
パラメータにはデフォルト値が設定されているため、省略するのが望ましいです。
enum StatusEnum
{
case Active;
case Deleted;
case Stopped;
}
class User
{
public function isInStatus(bool $excluded = false, StatusEnum ...$statuses): bool {}
}
isInStatus
を使用する場合、isInStatus(statuses: [StatusEnum::Deleted])
、isInStatus(statuses: [StatusEnum::Deleted], excluded: true)
のように使用できます。
メソッドを設計する際、類似グループのクラスであれば、excluded
のような共通のメソッドの定義位置は一貫性を持たせたいと思うでしょう。
問題は、isUpdatedBy
の場合はid
が直後に来て、excluded
が最後に来ると直感的に分かりやすく、isInStatus
でも同様に、statuses
が先に来てexcluded
が最後に来るのが自然です。しかし、可変パラメータのためにexcluded
を最後に配置することができません。
このように、どちらも bool $excluded = false
を共通して定義する場合でも、あるケースでは第2引数、別のケースでは第1引数にしなければならないことがあります。そのような場合、使用時に名前付き引数を使うことで、呼び出し方を一貫させることができます。
名前付き引数がない場合
名前付き引数がないと、isInStatus(false, ...[StatusEnum::Deleted, StatusEnum::Stopped])
のように使用しなければなりません。excluded
はデフォルトでfalse
なので省略したいところですが、名前付き引数がないと、パラメータ定義の順序通りに引数を渡す必要があるため、statuses
を渡すためにはexcluded
を省略できず、コードが洗練されなくなるという問題があります。
名前付き引数を使えば、excluded
引数を後ろに配置することで、デフォルト値が設定されたパラメータへの引数の指定を省略できるという利点があります。さらに、渡そうとしている引数の意味を明確にすることで、関数のパラメータ定義を直接見なくても、どのような意味の引数を渡しているのかを把握することができます。
また、デフォルト値の付いたパラメータは省略可能なので、次のように渡す必要のある引数だけを記述し、不要な情報を取り除いたコードを書くことができます。
isInStatus(statuses: ...[StatusEnum::Deleted, StatusEnum::Stopped], excluded: false);
isInStatus(statuses: ...[StatusEnum::Deleted, StatusEnum::Stopped]);
名前付き引数の問題点
名前付き引数を使用する場合、関数のパラメータ名を明示的に指定することになるため、関数のパラメータ名が変更されると、その関数を使用しているすべてのコードを修正しなければならないという問題があります。
そのため、すべての関数で名前付き引数を使うのではなく、関数名からどのような引数を渡せばよいかが明確に分かる場合には、無理に使用しない方がよいでしょう。逆に、関数名だけではどのパラメータを何番目に渡すべきかが分かりにくい場合や引数の順序を変える必要がある場合には、名前付き引数を使うのが適しています。
人間の言語のように表現する
名前付き引数を使うことで、関数の引数のうちデフォルト値が設定されているものを省略でき、英語の文法上ではisUpdatedBy
の場合id
が先に来てexcluded
が後に来る方が語順として分かりやすい場合でも、引数の渡す順序を適切に入れ替えることができるため、人間の言語のような認知構造に沿った表現が可能になるという利点があります。
シングルアクションクラスとの比較
名前付き引数がなかった時代には、コードを読む際に関数の定義を直接確認したり、渡すパラメータの値を意味のある変数に代入してから渡す方法が使われていました。また別の方法としては、必要な値を setter で設定し、action メソッドひとつでオブジェクトを関数の代わりに使うパターンもありました。
1つのアクションを持つさまざまなクラスを定義することができますが、代表的な例としてはビルダーパターンが挙げられます。
class User
{
public function __construct(string $name, int $age, Gender $gendder, string $address) {}
}
enum Gender
{
case Male;
case Female;
}
class UserBuilder
{
private string $name = '';
private ?int $age = null;
private ?Gender $gender = null;
private string $address = '';
public function name(string $name): self
{
assert($this->name === '');
assert($name !== '');
$this->name = $name;
return $this;
}
public function age(int $age): self
{
assert($this->age === null);
assert($age >= 0);
$this->age = $age;
return $this;
}
public function gender(Gender $gender): self
{
assert($this->gender === null);
$this->gender = $gender;
return $this;
}
public function withAddress(string $address): self
{
assert($this->address === '');
assert($address !== '');
$this->address = $address;
return $this;
}
public function build(): User {
if ($this->name === '') {
throw new LogicException('name must be set');
}
if (is_null($this->age)) {
throw new LogicException('age must be set');
}
if (is_null($this->gender)) {
throw new LogicException('gender must be set');
}
return new User($this->name, $this->age, $this->gender, $this->address);
}
}
(new UserBuilder)->name('yamada')->age(30)->gender(Gender::Male)->build();
上記のように、actionはbuildメソッドひとつだけで、必要な値はsetterを通じて受け取っていました。
さいごに
PHPに名前付き引数がなかった時代には、関数の定義を確認したり、可読性のためにシングルアクションクラスを使ったり、引数をパラメータの意味を持つ変数に代入してから渡すなどの方法が用いられていました。しかし、PHPに名前付き引数が導入されてからは、より簡潔で可読性の高い引数の渡し方ができるようになりました。
名前付き引数を使えば、デフォルト値を省略して不要な情報を隠し、引数の順番を変えて渡すことができ、人間の言語に近い順序でコードを書くことができるため、コードを読む際に文章を読むような自然さと心地よさを与えてくれます。必要に応じて積極的に活用することで、コードの可読性を向上させることができます。