2021.12.28
STAFF BLOG
スタッフブログ
TECHNICAL
テクログ
タイトルの通りです。
調べてもそれらしい記事がなかったので書こうと思います。
repとは
まず、repについて説明します。
repとは、主に競技プログラミングにおいてC++などで使用されているマクロになります。
マクロについての詳しい説明は省きますので、気になる方は以下の記事などを参考にしてください。
https://atcoder.jp/contests/apg4b/tasks/APG4b_an?lang=ja
さて、repが使えると嬉しいことがあります。
それは、for文の入力文字数を削減できることです。
// (1)
for(int i = 0; i < n; ++i)
{
// 何らかの処理
}
// (2)
rep(i, n)
{
// 何らかの処理
}
(1)はi=0~n-1のn回のループ処理を行います。
(2)はrepというマクロが使われていますが、意味する情報は(1)と同じです。
比較をすると一目瞭然ですが、(2)の方がより少ないコード量でfor文を記述することができます。
このrepマクロをPHPでも利用して、for文のコード量を減らしたいというのが今回のテーマです。
しかし、PHPはインタプリタ型言語のため、マクロを定義することができません。
よって、マクロを使わずにrepマクロのようなものを実装するにはどうしたらよいかを考えていきます。
repの関数化
まずは、repを関数化してみます。
<?php
// 関数fの処理をループ変数iでn回行う
function rep($n, $f)
{
for($i = 0; $i < $n; ++$i)
{
if($f($i)) break;
}
}
const _break = true; // rep中でreturnするとbreakできる
const _continue = false; // rep中でreturnするとcontinueできる
$n = 5;
rep($n, function($i)
{
echo $i.' ';
});
// -> 0 1 2 3 4
- repという関数を定義し、引数として受け取った関数fの処理をn回実行する
- repを実行するときは、関数fを無名関数などで生成して引数として渡してあげる
rep中でreturnすることはfor文のループを一つ進めることと同義です。
しかし、return trueのときはbreakするような分岐処理を書くことによって、rep中でもbreakやcontinueができるようになります。
次に、2重ループや値の更新処理についても見ていきます。
// 関数fの処理をループ変数iでn回行う
function rep($n, $f)
{
for($i = 0; $i < $n; ++$i)
{
if($f($i)) break;
}
}
const _break = true; // rep中でreturnするとbreakできる
const _continue = false; // rep中でreturnするとcontinueできる
$n = 10;
$multi_table = []; // 九九の表
rep($n, function($i) use($n, &$multi_table)
{
rep($n, function($j) use($i, &$multi_table)
{
if($i * $j === 0)
{
return _continue;
}
$multi_table[$i][$j] = $i * $j;
});
});
print_r($multi_table);
これは九九の表を作成するプログラムです。
乗算した値が0の場合はcontinueをして、表の更新処理は行わないようにしています。
ここで少々面倒なのが、rep内で外部の変数を使用したい場合はuseに記述する必要がある点です。
また、更新処理を行いたい場合は、さらに変数の頭にアンド(&)をつけて参照を渡す必要があります。
以上のことからrepの関数化についてまとめてみます。
- PHPでは無名関数を利用することで疑似的にrepマクロを再現することができる。
- rep内でcontinueやbreakをすることも可能
- rep外の変数を使用したい場合はuseに記述する必要があるため冗長になる
PHPでrepの再現ができたところまではよかったのですが、使用したい変数をいちいちuseに書く作業がとても面倒です。
「コード量は多くなったけどPHPでrepが書けました。めでたしめでたし。」となっては本末転倒です。
よって次は、repは使わないけれどfor文を短く書くことができる記述方法について考えていきます。
range関数
range — ある範囲の整数を有する配列を作成する
https://www.php.net/manual/ja/function.range.php
公式リファレンスによるとこのような説明がされています。
range関数を使用すると以下のような配列が生成されます。
print_r(range(0, 5));
// -> [0, 1, 2, 3, 4, 5]
print_r(range('a', 'e'));
// -> ['a', 'b', 'c', 'd', 'e']
range関数が生成する配列をループ変数とみなすと、以下のようにループを記述することができそうです。
$n = 5;
foreach(range(0, $n - 1) as $i)
{
// 何らかの処理
echo $i.' ';
}
// -> 0 1 2 3 4
range関数が生成する配列は閉区間ですので、repと同じく半開区間となるように工夫をします。
そのためには、rangeの前に一つ関数をかましてあげればよいです。
// 半開区間のループに変える
function rep($n)
{
return range(0, $n - 1);
}
$n = 5;
foreach(rep($n) as $i)
{
// 何らかの処理
echo $i.' ';
}
// -> 0 1 2 3 4
また、少し手を加えることで逆順のループも実装することができます。
// 逆順のループ
function rrep($n)
{
return range($n - 1, 0);
}
$n = 5;
foreach(rrep($n) as $i)
{
// 何らかの処理
echo $i.' ';
}
// -> 4 3 2 1 0
これらは一見すると簡潔に記述ができていて、何ら問題はなさそうに見えます。
しかし、普通のfor文とは異なり、range関数を使用すると指定したサイズ(n)の配列を生成することになります。
つまり、その分だけメモリを消費することになります。
一部の競技プログラミングの問題を解くときなど、メモリサイズをあまり気にしない状況下ではコード量を減らすことができそうです。
しかし、webサイトを運用していく上では障害になりうるため、この書き方はあまり推奨されていないようです。
- range関数で生成した値をループ変数とみなすようなforeach文を書くことができる
- repなどの自作関数をかますことで半開区間に変えられる
- range関数の第1引数と第2引数を入れ替えることで逆順のループ処理をすることも可能
- メモリの使用量が増えるため非推奨
foreachと参照
配列内のすべての値を更新したいときなどは、参照を利用することでfor文を簡略化することが可能です。
たとえば、以下のコードは配列の累積和を求めるプログラムになります。
$array = [1, 2, 3, 4, 5];
$total = 0;
foreach($array as &$x)
{
$total += $x;
$x = $total;
}
print_r($array);
// -> 1 3 6 10 15
ループの中でインデックスに気を付ける必要がないことも利点の一つだと思います。
他には標準入力を受け取るときにも有効です。
標準入力を受け取る場合は、array_fill関数などを用いて必要な分だけ配列のサイズを確保しておく必要があります。
$n = 5;
$array = array_fill(0, $n, 0);
foreach($array as &$x)
{
$x = trim(fgets(STDIN));
}
print_r($array);
しかし、参照を利用するループ文では現在のインデックスからの相対的な位置にある配列の値を見に行くことができません。
たとえば、i番目のループのときにi+1番目の値にアクセスすることはできません。
どうしてもアクセスしたい場合は以下のように記述することでインデックスを参照することができますが、それならただのfor文でよいのではとなってしまいます。
foreach($array as $index => &$x)
そのため、配列内の相対的な位置関係によらない単純な更新処理に限り、この書き方は有効であると言えそうです。
- 配列内の全ての値を書き換えたいときなどに有効
- 現在のループのインデックスから相対的な位置にある配列の値を見に行くのには不向き
まとめ
- repを関数化することでPHPでも疑似的にrepを使用することができる
- range関数で生成した配列をforeach文で回すのはメモリの使用量が増えるため非推奨
- foreachと参照を組み合わせたループ文は配列の全ての値を更新したいときなどに有効