PHP/入門/関数とは

出典: フリー教科書『ウィキブックス(Wikibooks)』
< PHP
ナビゲーションに移動 検索に移動

関数の定義と呼出し[編集]

PHPでは、ひとまとまりの処理に名前をつけ、あとから何度も呼出せるようにする仕組みがあります。 これを関数(かんすう;【英】Function)といいます。 関数を作ることを、「関数定義」関数を使うことを「関数呼出し」といいます。

PHPには、すでに有用な関数が多数用意されており、これらを「組込み関数」と呼びます。

プログラマーは、組込み関数・文・式を組合わせて新しい関数を定義することができます。 新しく定義された関数を「ユーザー定義関数」と呼びます。

ユーザー定義関数の例(1)
<?php
function f() {
    echo "abc", PHP_EOL;
    echo "123", PHP_EOL;
}

f();
f();
f();

?>
実行結果
abc
123
abc
123
abc
123
ユーザー定義関数 f()を定義し、3回呼出しています。
関数定義の書式:
function 関数名(引数リスト) {
    // 処理
}

上のプログラムは、次のようにもかけます。

ユーザー定義関数の例(2)
<?php
f();
f();
f();

function f() {
    echo "abc", PHP_EOL;
    echo "123", PHP_EOL;
}

?>
関数の定義の前に、関数を呼出しを行っていますが、PHPの場合は「関数の前方参照」は処理系が自動的に解決するので、前方参照のための関数宣言は必要ありません(逆にできません)。
前方参照は処理系が解決してくれますが、人間が読むときには前方参照が多いと(定義を見て)引数/戻値やざっくりした処理を理解する前に関数呼出しが来てしまうと読解しづらいので、なるべく定義→呼出しの順になるようにすると良いでしょう。
正しく動くこと、、だけでなく読みやすさも大切です。

値を返す関数[編集]

関数は、呼出し元に値を返すことができます。 値を返すには return文を使い、returnは単一の式を取ります。 もし、returnに値を与えないと、その関数の戻値型は void 型となります。

例えば、下記のコードは、単純に関数fが1を返す関数です。

<?php
function f() {
    return 1;
}

echo f();

?>
実行結果
1
と表示されます。
参考
<?php
echo pi();
?>
実行結果
3.1415926535898 
pi() は、円周率の返す関数です。

関数の引数と戻値[編集]

PHPの関数は、0個以上の引数を渡し1つの戻値を返すことができます。 可変長引数や引数のディフォルト値、またキーワード引数もサポートしています。

<?php
declare(strict_types=1);

function add($i, $j) {
    return $i + $j;
}

echo 'add(20, 99) --> ', add(20, 99), PHP_EOL;

function sum(...$args) {
    $sum = 0;
    foreach ($args as $arg) {
        $sum += $arg;
    }
    return $sum;
}
echo 'sum(1,2,3,4,5,6,7,8,9,10) --> ', sum(1,2,3,4,5,6,7,8,9,10), PHP_EOL;
$ary = array();
for ($i = 1; $i <= 100; $i++) {
    $ary[$i] = $i;
}
echo 'sum(...$ary) --> ', sum(...$ary), PHP_EOL;

function acc($i, $j = 1) {
    return $i + $j;
}

echo 'acc(20) --> ', acc(20), PHP_EOL;
echo 'acc(20, 99) --> ', acc(20, 99), PHP_EOL;

function wishes($age, $name = 'You') {
    $postfix = 'th';
    switch ($age % 10) {
        case 1: $postfix = 'st'; break;
        case 2: $postfix = 'nd'; break;
        case 3: $postfix = 'rd'; break;
    }
    echo "Happy $age$postfix birthday to $name!", PHP_EOL;
}

wishes(13);
wishes(name:"Tom", age:21);
wishes(age:12, name:"Alice");
wishes(age:100, name:"My granny");

?>
実行結果
add(20, 99) --> 119
sum(1,2,3,4,5,6,7,8,9,10) --> 55
sum(...$ary) --> 5050
acc(20) --> 21
acc(20, 99) --> 119
Happy 13rd birthday to You!
Happy 21st birthday to Tom!
Happy 12nd birthday to Alice! 
Happy 100th birthday to My granny!
スプレッド構文を使えば可変長引数の関数も定義できます。
引数が省略されたときのディフォルト値を与えることもできます。
キーワード引数もサポートしています。

型宣言[編集]

型宣言は、関数の引数や返り値、そして PHP 7.4.0 以降ではクラスのプロパティに追加することができます。 これらは、呼出し時に指定した型の値であることを保証し、 一致しない場合は TypeError がスローされます。 strict_types を 1 にセットすると、型宣言漏れをエラーにできます。

<?php
declare(strict_types=1);

$y = 7;
$fn1 = fn(int $x) : int => $x + $y;
$fn2 = function (int $x) use ($y) : int {
    return $x + $y;
};
echo '$fn1(3) --> ', $fn1(3), PHP_EOL;
echo '$fn2(3) --> ', $fn2(3), PHP_EOL;
$y = 1;
echo '$fn1(3) --> ', $fn1(3), PHP_EOL;
echo '$fn2(3) --> ', $fn2(3), PHP_EOL;
?>
実行結果
10119
型宣言のある関数定義の書式:
function 関数名(引数型1 引数名1, 引数型2 引数名2,  引数型n 引数名n) : 戻値型 {
    // 処理
}

有効な型[編集]

プリミティブ型のほか、クラスやインターフェースの名前は、型名として認識されます。

PHPの関数の引数や戻値の型
説明 バージョン
classまたはinterfacd の名前 値は与えられたクラスまたはインターフェースのインスタンスでなければなりません。
self 値は型宣言が使用されているクラスと同じクラスのインスタンスでなければなりません。クラス内でのみ使用可能。
parent 型宣言が使われているクラスの親のインスタンスでなければなりません。クラス内でのみ使用可能。
array 値は配列でなければなりません。
callable 値は有効な callable である必要があります。クラスのプロパティの型宣言としては使用できません。
bool 値はブール値でなければなりません。
float 値は浮動小数点数でなければなりません。
int 値は整数値でなければなりません。
string 値は文字列でなければなりません。
iterable 値は、配列か Traversable のインスタンスでなければなりません。 PHP 7.1.0
object 値はオブジェクトでなければなりません。 PHP 7.2.0
mixed 値は任意の値であることができます。 PHP 8.0.0

変数のスコープ[編集]

PHPでは、名前空間のトップレベルで代入された変数をグローバル変数と呼び、関数内で代入された変数をローカル変数と呼びます。 PHPに変数宣言はないので、最初の代入が宣言に準じた扱いになります。 関数内のローカル変数は(static 宣言されなければ)動的で、関数を抜けると揮発します。 関数の中から、グローバル変数を参照するには、3つの方法があります。

<?php
declare(strict_types=1);

$i = 10;
echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;

function g() {
    global $i;
    echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;
    $i = 99;
}

g();

echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;

function a() {
    $i = &$GLOBALS['i'];
    echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;
    $i = 42;
}

a();

echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;

function r(&$i) {
    echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;
    $i = 4423;
}

r($i);

echo __FUNCTION__, ": \$i= {$i}", PHP_EOL;
?>
実行結果
: $i= 10
g: $i= 10
: $i= 99
a: $i= 99
: $i= 42
r: $i= 42 
: $i= 4423
__FUNCTION__ は、実行中の関数を文字列返すマジック定数です。
トップレベルでは、 ”” を返します。
g()
キーワード global で宣言されたグローバル変数は代入を含む参照ができます。
a()
$GLOBALSはグローバル変数を要素とするハッシュで、代入を含む参照ができます。
r()
グローバル変数へのリファレンスを関数の引数として渡すと、リファレンス経由で代入を含む参照ができます。

3つ方法がありますが、どれも使わないと関数からのグローバル変数の参照は PHP Warning: Undefined variable $eeeになり失敗します。

static変数[編集]

関数の内側で作られた変数は関数を抜けると揮発します。

関数から抜けても、次回の呼出しで値を保持してほしいときは、ローカル変数を static 宣言します。

<?php
declare(strict_types=1);

function sacc() : void {
    static $i = 0;
    echo __FUNCTION__, ': $i = ', $i, PHP_EOL;
    $i++;
}
sacc();
sacc();
sacc();
sacc();
?>
実行結果
sacc: $i = 0
sacc: $i = 1
sacc: $i = 2
sacc: $i = 3
関数内で関数を定義しても関数スコープにはならない
関数の中で別の関数を定義することは可能ですが意味はありません。
<?php
declare(strict_types=1);

function outer() {
    $y = 10;
    function inner() {
        global $y;
        echo __FUNCTION__, "\$y = $y", PHP_EOL;
    }
    inner();
    inner();
}

outer();
inner();

$y = 0;

// outer(); PHP Fatal error:  Cannot redeclare inner() (previously declared in /workspace/Main.php:7) in /workspace/Main.php on line 6
inner();
?>
実行結果
inner$y = 
inner$y = 
inner$y =  
inner$y = 0
関数の中で別の関数を定義することは可能ですが意味はありません。
定義した関数の中で inner() を呼出すことは複数回できます。
定義した関数を抜けたあとも、定義した関数を呼出すことはできます。
ですが、関数内で定義された関数から「外側の」ローカル変数は参照できません。
global 宣言した変数は、「外側の」ローカル変数ではなくトップレベルの変数です。
あまつさえ、関数を定義する関数を二回呼出すと、PHP Fatal error: Cannot redeclare と致命的エラーです。
これは、関数内で別の関数を定義しても関数スコープにはならずクロージャではないことを表しています。


無名関数[編集]

関数の中で関数を定義してもクロージャーにはなりませんが、無名関数でクロージャーを実現できます。 PHPには、無名関数を定義する方法に、アロー関数と関数式の2つがあります。

<?php
$y = 7;
$fn1 = fn($x) => $x + $y;
$fn2 = function ($x) use ($y) {
    return $x + $y;
};
echo '$fn1(3) --> ', $fn1(3), PHP_EOL;
echo '$fn2(3) --> ', $fn2(3), PHP_EOL;
$y = 1;
echo '$fn1(3) --> ', $fn1(3), PHP_EOL;
echo '$fn2(3) --> ', $fn2(3), PHP_EOL;
?>
実行結果
$fn1(3) --> 10
$fn2(3) --> 10
$fn1(3) --> 10
$fn2(3) --> 10
fn($x) => $x + $y;=>が矢印っぽいのでアロー関数と呼ばれます。
この記号は { 'a' => 1 } (連想配列)でも使われます。
function ($x) use ($y) { return $x + $y;};は、関数式です。
関数定義とよく似ていますが名前がありません。
use でキャプチャーする環境を指定します。
キャプチャー
上の実行結果をよく見ると9行目 $y = 1;のあとも答えが変わっていません。
このようにアロー関数や関数式(まとめて無名関数)は、定義されたときの環境をキャプチャーします。
このようキャプチャーを行う性質から、無名関数は、しばしまクロージャーと呼ばれます。
無名関数の中で __FUNCTION__ を参照すると、{closure} となります。

再帰的呼出し[編集]

関数は、自分の定義の中に自分を含めることができます。

<?php
declare(strict_types=1);

function ipow(int $x, int $y) : int {
    if ($y == 0)
        return 1;
    if ($y == 1)
        return $x;
    return $x * ipow($x, $y - 1);
}

echo 'ipow(2, 3) --> ', ipow(2, 3), PHP_EOL;
echo 'ipow(10, 9) --> ', ipow(10, 9), PHP_EOL;

function comma3(int $n) {
    if ($n < 0) {
        return "-" . comma3(-$n);
    }
    $num = intdiv($n, 1000);
    $rem = $n % 1000;
    if ($num) {
        return comma3($num) . sprintf(",%03d", $rem);
    }
    return sprintf("%d", $rem);
}
echo 'comma3(ipow(10, 9)) --> ', comma3(ipow(10, 9)), PHP_EOL;
echo 'comma3(ipow(2, 32)) --> ', comma3(ipow(2, 32)), PHP_EOL;
実行結果
ipow(2, 3) --> 8
ipow(10, 9) --> 1000000000
comma3(ipow(10, 9)) --> 1,000,000,000 
comma3(ipow(2, 32)) --> 4,294,967,296
整数の累乗を求める関数 ipow() と、整数を3桁ごとに ',' で区切ってた文字列を返す関数 comma3() を定義しました。
2つの関数とも定義が再帰的なので、再帰的呼出しで実装しています。
再帰的呼出しは、終了条件を間違えると「無限再帰」に陥ってしまい、資源(この場合はスタック)を喰いつくすまで帰ってきません。
アルゴリズムが再帰的な場合、再帰的呼出しを行う関数として実装するとコードは短くなりますが、PHPの処理系はtailrecのような最適化は行わないので、関数呼出しのオーバーヘッドが再帰のたびに課金され、資源にも優しくないので、再帰で実装したあと回帰テストを書き、ループで実装しテストを通ることを確認するのがベターでしょう。

ジェネレター[編集]

ジェネレター関数は、値を返す代わりに、yieldで必要なだけの値を生成することを除けば、通常の関数と同じように見えます。 yieldを含む関数はすべてジェネレター関数です。

ジェネレター関数が呼出されると、繰返し実行可能なオブジェクト(iterable型のオブジェクト)が返されます。 そのオブジェクトに対して反復処理を行うと (たとえば foreach ループで)、 PHP は値が必要になるたびにオブジェクトの反復処理用メソッドをコールし、 ジェネレターが値を返した時点でその状態を保存して、次の値が必要になったときに再開できるようにします。

生成する値がなくなったら、ジェネレターは単に終了し、呼出し元のコードは配列の値がなくなったときと同じように続行できます。

<?php
declare(strict_types=1);

function gen_square() : iterable {
    for ($i = 0; $i < 10; $i++) {
        yield $i * $i;
    }
}

foreach (gen_square() as $value) {
    echo "$value\n";
}
var_export(array(...gen_square()));
実行結果
0
1
4
9
16
25
36
49
64
81
array (
  0 => 0,
  1 => 1,
  2 => 4,
  3 => 9,
  4 => 16,
  5 => 25,
  6 => 36,
  7 => 49,
  8 => 64,
  9 => 81,
)

ジェネレターは配列に似ていますが、配列は 要素数 × 要素1つあたりのサイズ のメモリーフットプリントを消費します。それに対しジェネレターは、yield で返す値は都度計算できるのでメモリー消費は配列に比べ小さくなります。

コードキャラリー[編集]

まとまった規模で、実用的な目的に叶うコードを読まないと機能の存在理由などの言語設計に思い至りにくいので、コードの断片から少し踏み込んだプログラムを掲載します。

array_*関数の拡張[編集]

PHPの組込み関数には、array_reduce(),array_map() のように array_ はじまる配列関数群があります。 これらは便利なのですが、配列とハッシュ兼用でJavaScriptやRubyなどから来ると面食らうことがあります。 また、配列の添字がcallback関数にわたらない仕様も移植のときに問題になります。 そこで、上記の問題をクリアした配列専用の関数群を作ってみました。 また、sum() は、カハンの加算アルゴリズムを使い誤差を最小にするようにしています。

実装
<?php
// phpinfo();
declare(strict_types=1);

function reduce(array $ary, callable $code, mixed $result = null): mixed {
    $i = 0;
    foreach ($ary as $el) {
        if ($i == 0 && !isset($result)) {
            $result = $el;
        } else {
            $result = $code($result, $el, $i);
        }
        $i++;
    }
    return $result;
}
echo 'reduce([1, 2, 3], fn($s, $x) => $s + $x) --> ', reduce([1, 2, 3], fn($s, $x) => $s + $x), PHP_EOL;
echo 'reduce([1, 2, 3], fn($s, $x) => $s + $x, 10) --> ', reduce([1, 2, 3], fn($s, $x) => $s + $x, 10), PHP_EOL;
echo 'reduce(["A", "B", "C"], fn($s, $x) => $s . $x) --> ', reduce(["A", "B", "C"], fn($s, $x) => $s . $x), PHP_EOL;
echo 'reduce(["A", "B", "C"], fn($s, $x) => [ ...$s, "<$x>"], []) --> ', var_export(reduce(["A", "B", "C"], fn($s, $x) => [...$s , "<$x>"], []), true),PHP_EOL;
echo PHP_EOL;

function map(array $ary, callable $code): array {
    $result = [];
    $i = 0;
    foreach ($ary as $el) {
        $result[] = $code($el, $i++);
    }
    return $result;
}
$a1 = [2, 3, 5, 7, 11];
echo '$a1 = [2, 3, 5, 7, 11];', PHP_EOL;
echo 'map($a1, fn($x) => $x * $x) --> ', var_export(map($a1, fn($x) => $x * $x), true), PHP_EOL;
echo PHP_EOL;

function each(array $ary, callable $code): void {
    $i = 0;
    foreach ($ary as $el) {
        $code($el, $i++);
    }
}
echo 'each($a1, fn($x, $i) => printf("%d[%d] ", $x, $i)); --> ';
each($a1, fn($x, $i) => printf("%d[%d] ", $x, $i));
echo PHP_EOL;
echo PHP_EOL;

function filter(array $ary, callable $code): array {
    $result = [];
    $i = 0;
    foreach ($ary as $el) {
        if ($code($el, $i++)) {
            $result[] = $el;
        }
    }
    return $result;
}
echo 'filter($a1, fn($x) => $x % 2) --> ', var_export(filter($a1, fn($x) => $x % 2), true), PHP_EOL;
echo 'filter($a1, fn($x) => $x < 6) --> ', var_export(filter($a1, fn($x) => $x < 6), true), PHP_EOL;
echo PHP_EOL;

function sum(array $ary): int|float|null {
    $sum = null;
    $i = 0;
    foreach ($ary as $delta) {
        if ($i == 0) {
            $sum = $delta;
            $c = $delta - $delta;
        } else {
            $y = $delta - $c;
            $t = $sum + $y;
            $c   = ( $t - $sum ) - $y;
            $sum = $t;
        }
        $i++;
    }
    return $sum;
}
$nums = array_fill(0, 10, 0.1);
echo '$nums = array_fill(0, 10, 0.1);', PHP_EOL;
echo 'reduce($nums, fn($x, $y) => $x + $y) --> ', sprintf("%.53f", reduce($nums, fn($x, $y) => $x + $y)), PHP_EOL;
echo 'array_sum($nums) --> ', sprintf("%.53f", array_sum($nums)), PHP_EOL;
echo 'sum($nums) --> ', sprintf("%.53f", sum($nums)), PHP_EOL;
echo 'sum(range(1, 100)) --> ', sprintf("%d", sum(range(1, 100))), PHP_EOL;
echo 'sum([]) --> ',var_export(sum([]), true), PHP_EOL;
echo PHP_EOL;

function every(array $ary, callable $code): bool {
    $i = 0;
    foreach ($ary as $el) {
        if (!$code($el, $i++)) {
            return false;
        }
    }
    return true;
}
echo 'every($a1, fn($x) => $x != 1) --> ', var_export(every($a1, fn($x) => $x != 1), true), PHP_EOL;
echo 'every($a1, fn($x) => $x == 2) --> ', var_export(every($a1, fn($x) => $x == 2), true), PHP_EOL;
echo PHP_EOL;

function some(array $ary, callable $code): bool {
    $i = 0;
    foreach ($ary as $el) {
        if ($code($el, $i++)) {
            return true;
        }
    }
    return false;
}
echo 'some($a1, fn($x) => $x != 1) --> ', var_export(some($a1, fn($x) => $x != 1), true), PHP_EOL;
echo 'some($a1, fn($x) => $x == 2) --> ', var_export(some($a1, fn($x) => $x == 2), true), PHP_EOL;
echo PHP_EOL;
実行結果
reduce([1, 2, 3], fn($s, $x) => $s + $x) --> 6
reduce([1, 2, 3], fn($s, $x) => $s + $x, 10) --> 16
reduce(["A", "B", "C"], fn($s, $x) => $s . $x) --> ABC
reduce(["A", "B", "C"], fn($s, $x) => [ ...$s, "<$x>"], []) --> array (
  0 => '<A>',
  1 => '<B>',
  2 => '<C>',
)

$a1 = [2, 3, 5, 7, 11];
map($a1, fn($x) => $x * $x) --> array (
  0 => 4,
  1 => 9,
  2 => 25,
  3 => 49,
  4 => 121,
)

each($a1, fn($x, $i) => printf("%d[%d] ", $x, $i)); --> 2[0] 3[1] 5[2] 7[3] 11[4] 

filter($a1, fn($x) => $x % 2) --> array (
  0 => 3,
  1 => 5,
  2 => 7,
  3 => 11,
)
filter($a1, fn($x) => $x < 6) --> array (
  0 => 2,
  1 => 3,
  2 => 5,
)

$nums = array_fill(0, 10, 0.1);
reduce($nums, fn($x, $y) => $x + $y) --> 0.99999999999999988897769753748434595763683319091796875
array_sum($nums) --> 0.99999999999999988897769753748434595763683319091796875
sum($nums) --> 1.00000000000000000000000000000000000000000000000000000
sum(range(1, 100)) --> 5050
sum([]) --> NULL

every($a1, fn($x) => $x != 1) --> true
every($a1, fn($x) => $x == 2) --> false

some($a1, fn($x) => $x != 1) --> true
some($a1, fn($x) => $x == 2) --> true