先日投稿した以下の記事に面白いコメントが寄せられたので記事を書くことにしました。
7²+8²+9²+...17²+18²=2018(年)になるというネタがホントかどうかChromeで検算する
for文の解説から始めて最後はreduceを使ったワンライナー
console.log([...Array(12)].reduce((prev, _, i) => prev + (i + 7) ** 2, 0))
を紹介したのですがなんと、@amanoeseさんが圧倒的に短いワンライナーをコメント欄に残してくれました。
console.log((f=x=>x>6?x**2+f(x-1):0)(18))
今回は非常にシンプルで不思議なこのワンライナーの構造を解説します。
複数行に書き直してみる
console.logは無視して、中身を紐解いていきます。
(f=x=>x>6?x**2+f(x-1):0)(18)
1文字目は代入式の結果を返すためのカッコですね。
2文字目の「f」は変数です。
3文字目の「=」は代入。
4文字目は引数です。アロー関数の記述ルールに「引数が1つのときはカッコを省略できる」というのがあります。
5,6文字目の「=>」はアロー関数。
7文字目以降は式ですね。計算結果を即時returnする場合アロー関数の「{}」を省略できます。
最後の「(18)」は直前のカッコが返す関数に18を引数として渡して呼び出してます。
一旦ここまでの情報で書き直します。
const f = function(x) {
return x > 6 ? x ** 2 + f(x - 1) : 0;
};
f(18);
おお、ずいぶん読みやすくなりました。
2行目の「____ ? ____ : ____ 」は3項演算子と呼びます。
「++ ____ 」は単項演算子、
「____ + ____ 」は2項演算子、
「____ ? ____ : ____ 」は3項演算子です。
わかりやすいですね。
3項演算子は以下のようにif文で置き換えられます。
const f = function(x) {
let result;
if (x > 6) {
result = x ** 2 + f(x - 1);
} else {
result = 0;
}
return result;
};
f(18);
これで全体像が見えてきました。
最後の難関は5行目
result = x ** 2 + f(x - 1);
ここに今日のテーマ、再帰関数が使われています。
再帰関数
関数「f」が内部でまた自分自身「f」を呼んでいるので「fは再帰関数である」というだけのことなので定義は簡単です。
しかし!一目見て何をやっているのかわからないし、自分で書ける気がしないですよね・・・
再帰関数は通常4つの手順で構成されています。
- 計算する
- 引数を変えて自分を呼ぶ
- 無限に自分を呼び続けないように条件式で止める
- 計算結果をreturnする
再帰は繰り返し計算を扱う概念なので今回の問題を繰り返しで分解します。
7²+8²+9²+...16²+17²+18²
↓
「7²」+「8²」+「9²」+...「16²」+「17²」+「18²」
さらに今回のワンライナーは引数が「18」から始まっていたので18を基準に式変換します。
「(18 - 11)²」+「(18 - 10)²」+「(18 - 9)²」+...「(18 - 2)²」+「(18 - 1)²」+「18²」
↓ 左右反転
「18²」+「(18 - 1)²」+「(18 - 2)²」+...「(18 - 9)²」+「(18 - 10)²」+「(18 - 11)²」
ここまで出来たら、最初の引数「18」を基準に3つの手順を埋めていきましょう。
①計算する
計算部分は最初の繰り返し要素「18²」だけに着目します。
引数xに18が代入されているので置き換えましょう。
「18²」
↓
「x²」
↓
「x ** 2」
計算はこれでおしまい。
②引数を変えて自分を呼ぶ
関数「f」は「引数を2乗して返す」という性質が①でわかりました。
繰り返し要素2つ目以降
... +「(18 - 1)²」+「(18 - 2)²」+...「(18 - 9)²」+「(18 - 10)²」+「(18 - 11)²」
の変化に着目すると、「最初の引数18を繰り返しのたびに1ずつ減らしながら2乗してる」というのがわかりますね。
ここで再帰を使います。
引数を1ずつ減らしながら
x - 1
「引数を2乗して返す」性質がある自分自身に渡してあげる!
f(x - 1)
①の計算結果に、自分で自分を呼び出した結果を足し合わせると、
x ** 2 + f(x - 1)
おお、難関だった5行目が完成しました!
let result;
result = x ** 2 + f(x - 1);
「自分を呼び続けたらどうなるか」ではなく「繰り返すたびに起こる変化」に着目するのがポイントです!
③無限に自分を呼び続けないように条件式で止める
再帰はどこかで止めてあげないと無限に計算を続けてしまいます。
今回は18から始まる引数xを7で止めたいので
x > 6
という条件式を使ってif文を書きます。
if (x > 6) {
// 再帰する
} else {
// 再帰止める
}
再帰が止まる瞬間(つまりxが6のとき)にresultに代入しないままだと「undefined」がreturnされてエラーを起こしてしまいます。
そこで、計算結果に影響を与えない「0」を返します。
再帰で文字列を結合する場合は空文字「""」を返すこともあります。
let result;
if (x > 6) {
result = x ** 2 + f(x - 1);
} else {
result = 0;
}
④計算結果をreturnする
ここは解説不要ですね。
let result;
if (x > 6) {
result = x ** 2 + f(x - 1);
} else {
result = 0;
}
return result;
外側の関数「f」に、
const f = function(x) {
};
f(18);
合体させて完成!
const f = function(x) {
let result;
if (x > 6) {
result = x ** 2 + f(x - 1);
} else {
result = 0;
}
return result;
};
f(18);
これをアロー関数使って、3項演算子使って、カッコで関数本体を返してすぐ呼び出すと
(f=x=>x>6?x**2+f(x-1):0)(18)
になるわけです。
(最初の引数を12から始めなかったのは文字数が多くなるからでしょう)
お疲れ様でした!
ワンライナー書いてくれた人
@amanoese さんです。
最初の投稿の3時間後にはさらに3文字短いコードを書いてくれました。
console.log((f=x=>x>6&&x*x+f(x-1))(18))
読み解きの手順は同じですが、「x>6&&」がトリッキーで面白いです。
数値計算式に「true/false」を混ぜるとどんな挙動になるのか分解して確かめてみてください。
(JavaScript特有の現象なのだろうか・・・?)
個人的な感想
軽いノリで書き始めてみたら、超苦しかった・・・
やはり再帰関数は難しいですね。
実のところ、私は再帰関数をこの記事のようには深く考えず感覚で書いてきたので論理立てて説明しようとすると急に複雑に見えてしまいました。
とはいえ繰り返し1回分の計算式書いて、引数変化させて再帰して、止める条件書いてreturnすれば大体思った通りに動きます。
ちょっと苦しいけど慣れると非常にファイル探索とかゲームの連鎖(オセロの複数めくりとか)を実装するのに便利ですよ!