唐揚げはどこへ
オブジェクト指向とは。 関数型とは。 みたいなのを辿っていく中で見つけた、2016年の Qiita の記事から。
唐揚げ弁当がいくつかあるとします。それぞれ唐揚げが複数入っています。 この中からx個の唐揚げをつまみ食いするプログラムを作りましょう。
つまみ食いはバレないようにするために、その時点で最も唐揚げ数が多いお弁当から取るという仕様にします。
元記事ではこの課題に対して、まずはオブジェクト指向で、次いで関数型で、 JavaScript による実装を見せていた。 関数型のメリットを示したかったようだが、課題の所為か、実装の所為か、あまり成功しているようには見えないが。
それと、課題の
つまみ食いする
に対する実装が、つまみ食いされた結果の状態表示だけってのはどうなんだとも思う。
結果の状態はデバッグには役に立つが、しかしプログラムで示すべきは、つまみ食いの成功/失敗ではないかと。
元記事にはたくさんコメントがついている。 自分だったらこうするといった実装例も多数。 しかし元記事に誘導されたのか、状態(の変化)を表示するものばかりだった。 そこに注目してしまう気持ちは判るけど、それでいいのか。
まあ失敗するのは、つまり 「つまみ食いできなかった」 となるのは、これ以上やったらバレるって状態だからね。 つまみ食いはするがバレたくはない人のモデル化なら、大量につまみ食いしないって前提もありだろう。 それでも、どの弁当も残り0になってはいけないという制約は必要だと思う。 唐揚げが入ってないなら、それはもう唐揚げ弁当ではないのだから。
と、微妙にケチをつけた上で、自分でもやってみる。
01
まずは元記事同様、結果の状態を表示するだけ。
function pick01( orgLst, pickCnt ) {
if ( !pickCnt ) return orgLst;
const pickedLst = orgLst.toSorted( ( n1, n2 ) => n2 - n1 ).map( ( n, i ) => !i ? n - 1 : n );
return pick01( pickedLst, pickCnt - 1);
}
console.log( pick01( [ 10, 8, 6 ], 5 ) ); // 出力: [6, 7, 6]
やってることはごく単純な再帰処理。
- 唐揚げの個数の配列を降順にソートして、
- 先頭要素の値から1引いて事後の配列を作る。
- まだ続けるなら、事後情報を引数に自分自身を呼び出す。
元記事との違いは、唐揚げ弁当の並び順を保持しないこと。 保持するかどうかは、課題に明記されてないのでどうでもいいはずだし。
02
元の唐揚げ弁当の並び順を保つならどうなるかも気になったので、やってみる。
function pick02( orgLst, pickCnt ) {
if ( !pickCnt ) return orgLst;
const pickedLst = orgLst
.map( ( n, i ) => { return { num: n, idx: i } } )
.toSorted( ( o1, o2 ) => o2.num - o1.num )
.map( ( o, i ) => !i ? { num: o.num - 1, idx: o.idx } : o )
.toSorted( ( o1, o2 ) => o1.idx - o2.idx )
.map( o => o.num );
return pick02( pickedLst, pickCnt - 1 );
}
console.log( pick02( [ 10, 8, 6 ], 5 ) ); // 出力: [6, 7, 6]
やってることは前のとだいたい同じだが、順番を保つための手間が増えている。
- 唐揚げの個数の配列を、個数と元の順番を要素にもつオブジェクトの配列にし、
- 個数の降順で並べ替えて、
- 先頭の個数から1引いて事後の配列を作り、
- 今度は元の順番の昇順に並べ替えて、
- 個数の配列に変換する。 これが最終的な事後情報。
- まだ続けるなら、事後情報を引数に自分自身を呼び出す。
最終的に元の順番になる。 途中は知らん。 という考え方だが、何も問題無いはず。
オブジェクトの配列にする。 個数の配列に戻す。 それらを適切な名前をつけた関数にした方が読みやすくなるかとも思ったが、この程度ならコメントつけておけば十分だと思い直した。 まあ、コメントは無いのだが。
03
望むだけのつまみ食いができたかどうかを返すようにしてみる。 結果の状態も知りたいので、それも併せて。
function pick03( orgLst, pickCnt ) {
if ( !pickCnt ) return { nums: orgLst, done: true };
const pickedLst = orgLst.toSorted( ( n1, n2 ) => n2 - n1 ).map( ( n, i ) => !i ? n - 1 : n );
if ( !pickedLst[ 0 ] ) return { nums: orgLst, done: false };
return pick03( pickedLst, pickCnt - 1 );
}
console.log( pick03( [ 10, 8, 6 ], 5 ) ); // 出力: {nums: [6, 7, 6], done: true}
console.log( pick03( [ 10, 8, 6 ], 25 ) ); // 出力: {nums: [1, 1, 1], done: false}
実装が簡単なので最初のをベースにした。 成功/失敗の判断基準は以下の通り。
- もう続ける必要が無い状態まで来ていたなら成功。
- 先頭から一つ取ったら無くなってしまったら失敗。
全バージョン通してだが、名前が微妙なのには目を瞑る。
オブジェクト指向はどこへ
オブジェクト指向でやったらどうなるかを一瞬考えたが、一瞬で止めた。 課題が向いてないと思う。
関数型って、所謂手続型の処理を書き換えて、見通しが良くなったとか、安全になったとか、テストし易くなったとか、アピールし易いよね。 簡単なサンプルでも大丈夫。
でもオブジェクト指向だと、簡単なサンプルだと効果が判らない。 コード量は増えるし。 見通しは悪くなるし。 この方が変化に強いと言われても、今一つ実感できないし。
まあ、練習のためのサンプルに何を言うのかって話だが。