はじめに
おあそびで配列の派生を作る遊び。
function setup() {
createCanvas(400, 400);
const a0 = LoopArray.of(1,2,3,4,5);
const h0 = [];
for(let k=-30; k<30; k++){
h0.push(a0.get(k));
}
console.log(h0);
const a1 = RoundRobinArray.of(1,2,3,4,5);
a1.reset(true);
const h1 = [];
for(let k=0; k<30; k++){
h1.push(a1.pick());
}
console.log(h1);
a1.reset();
const h2 = [];
for(let k=0; k<10; k++){
h2.push(a1.pick());
}
console.log(h2);
const a2 = RandomChoiceArray.of("apple", "banana", "lemon", "grape", "dragonfruit", "peach", "kiwi");
a2.reset(true);
let testString = a2.pick();
draw = () => {
textSize(24);
textAlign(CENTER,CENTER);
background(0);
noStroke();
fill(255);
if(frameCount % 60 === 0){ testString = a2.pick(); }
text(testString, 200, 200);
}
}
class ArrayWrapper extends Array{
constructor(){
super(...arguments);
}
static from(){
// fromで配列を作った後、それを持つLoopArrayを構成すればいい。
const b = Array.from(...arguments);
const c = new this();
c.push(...b);
return c;
}
static of(){
// fromで配列を作った後、それを持つLoopArrayを構成すればいい。
// もしこれを元に作ってしまうと例の問題が発生してしまう。
const b = Array.of(...arguments);
const c = new this();
c.push(...b);
return c;
}
}
class LoopArray extends ArrayWrapper{
constructor(){
super(...arguments);
}
get(index){
if(this.length === 0){ return null; }
const L = this.length;
if(index > 0){
return this[index % L];
}else if(index < 0){
return this[(L - (-index % L)) % L];
}
return this[0];
}
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
}
// resetの際にtrueを指定すると延々と値を出力し続ける
// loopがfalseの場合はresetしない限りnull出すだけの代物になる
// 区切りが分かることが重要
// 旧SweepArrayの役割はこれが果たせるので、廃止する。
class RoundRobinArray extends ArrayWrapper{
constructor(){
super(...arguments);
this.loop = false;
this.index = 0;
}
reset(loop = false){
this.loop = loop;
this.index = 0;
}
pick(){
if(this.length === 0){ return null; }
const L = this.length;
if(this.index >= L){
return null;
}
const v = this[this.index];
this.index++;
if(this.index >= L){
if(this.loop){
// ここはloop前提のため、引数にtrueを指定しなければならない。
// 止まってしまう。事前に気づけて良かった。
this.reset(true);
}
}
return v;
}
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
}
class RandomChoiceArray extends ArrayWrapper{
constructor(){
super(...arguments);
this.loop = false;
this.indices = [];
}
reset(loop = false){
this.loop = loop;
this.indices.length = 0;
const L = this.length;
if(L === 0) return;
// 雑にシャッフル。小数部分で比較して整数取るのが楽。
const src = Array.from("0".repeat(L), (x, i) => i + Math.random()/2).sort((a, b) => Math.sign((a % 1) - (b % 1)));
// 多分mapの方が楽
this.indices = src.map(a => Math.floor(a));
}
pick(){
if(this.length === 0){ return null; }
if(this.indices.length === 0){
return null;
}
const v = this[this.indices.pop()];
if(this.indices.length === 0){
if(this.loop){
// ここはloop前提のため、引数にtrueを指定しなければならない。
// 止まってしまう。事前に気づけて良かった。
this.reset(true);
}
}
return v;
}
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
}
ArrayWrapper
配列の派生はFALさんがやっていて、面白そうだったので真似して書いてみたのがだいぶ前。自分なりに再構成した。そこまで複雑なことはしてない。
ArrayWrapperのコンストラクタはArrayのそれをそのままいただいている。ついでにstaticでfromとofを継承してる。この2つはArrayのコンストラクタとして代表的なもので、fromはイテラブルオブジェクトから新しい配列を作るもの、ofは列挙された数をナイーブに解釈して配列を作るもの。Arrayコンストラクタは単なる単一の数(「4」とか)を使ってその個数のnull配列を作る機能があるが、列挙されているとみなせない落とし穴がある。そこで、このような仕組みが用意されている。
Array.of() と Array() コンストラクターの違いは整数引数の扱いにあります。 Array.of(7) は単一の要素、7 を持つ配列を作成しますが、 Array(7) は length プロパティが 7 の空の配列を作成します(これは undefined の値を持つ実在のスロットではなく、7 つの空のスロットの配列を意味します)。
以降、これを継承してクラスを作っていくが、そのすべてにfromとofがある。ただしこんな長ったらしい処理をいちいち書いてはいない。superで継承してる。staticでsuperを呼び出すと継承元のクラスを呼び出すことができ、その定義文における「this」はこのクラスそのものという扱いになるので、全く同じ効果が期待できる。DRYは絶対ではないけど、こういうときはなるべく守った方がいいですね。
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
LoopArray
内容はシンプルで、たとえば配列が0~4のindexを持つ(長さ5)ときに、勝手なindexに対して、それを0~4の範囲にモジュロでいじって配列から値を取り出すわけ。FALさんが音楽のスケールで遊ぶコードを書いていてリメイクしたんですが:
その際に不便だったので作りました。
const a0 = LoopArray.of(1,2,3,4,5);
const h0 = [];
for(let k=-30; k<30; k++){
h0.push(a0.get(k));
}
console.log(h0); // 1,2,3,4,5が12ループ繰り返される
負の数でもどんとこいです。これ無いと記述が面倒になる。あのね、こういうところをサボるとメインの複雑なロジックを読み解くうえで妨げになるんです。歩道の石ころを蹴り飛ばして取り除くようなものです。邪魔なんですよね。
RoundRobinArray
与えられた配列の中身を0から順番に取り出していき、最後まで行ったらまた最初に戻ります。それはresetの際にフラグで指定します。デフォルトはfalseで、この場合すべて取り出すとそれ以降はnullを返すようになります。trueの場合、最初に戻り、延々と続きます。
重要なことは値を取り出す処理がpick()で、indexを使わないことです。つまり実行するたびにイテレートされていくので、わざわざindexを外部的に用意する必要がありません。最初に戻るように余計なコードを書く必要もありません。またフラグがfalse(デフォルト)の場合止まるんですが、それにより処理の終了を判断したりできます。なのでいずれの仕様にも役割があります。
const a1 = RoundRobinArray.of(1,2,3,4,5);
a1.reset(true);
const h1 = [];
for(let k=0; k<30; k++){
h1.push(a1.pick());
}
console.log(h1); // 1,2,3,4,5が6ループ
a1.reset();
const h2 = [];
for(let k=0; k<10; k++){
h2.push(a1.pick());
}
console.log(h2); // Array(10) [ 1, 2, 3, 4, 5, null, null, null, null, null ]
RandomChoiceArray
仕様がRoundRobinArrayとほぼ同じですが、こちらはresetの際にindex配列をシャッフルしたものを生成し、それに従って取り出していきます。取り出し終わったらloopのフラグに従ってresetしたりしなかったりします。毎回ランダムに中身を出してもいいんですが、それだとムラが気になるのでこうしています(そもそもその程度ならArrayWrapper要らない...)。というか別の用途があって、たとえばfalseの場合nullになって止まるわけですが、処理のたびに中身をランダムに取り出して取りつくしたい場合にこの仕様じゃないとまずいんですね。なので半分以上はこっちの都合で作っています。
シャッフルではちょっと遊びました。まあイテラブルなら何でもいいのです。
// 雑にシャッフル。小数部分で比較して整数取るのが楽。
const src = Array.from("0".repeat(L), (x, i) => i + Math.random()/2).sort((a, b) => Math.sign((a % 1) - (b % 1)));
// 多分mapの方が楽
this.indices = src.map(a => Math.floor(a));
おそらく無数にやり方があるんでしょうが今回は小数部分で比較する方法にしました。別にこんなところでアイデア発揮しても仕方ないので、どうでもいいです。
おわりに
ここまでお読みいただいてありがとうございました。先ほど紹介したコードを見ればわかることですが、所詮どれもただの部品でしかないです。しかしこの手の部品があることでメインロジックの構築に集中できます。ありがたいことです。
追記:nullが欲しい場合
まあレアケースですが、配列内にnullがあってそれも使いたい時に判定で問題が発生しますね。今の仕様は継続するとして、nullも使いたい場合の代替手段を用意しましょう(そのうち)。やり方はいくつあってもいいものです。一つしかないと詰みやすいのでどっちがいいという話にはなりません。
改善案
こうしました。
// resetの際にtrueを指定すると延々と値を出力し続ける
// loopがfalseの場合はresetしない限りnull出すだけの代物になる
// 区切りが分かることが重要
// 旧SweepArrayの役割はこれが果たせるので、廃止する。
class RoundRobinArray extends ArrayWrapper{
constructor(){
super(...arguments);
this.loop = false;
this.index = 0;
this.returnable = false; // 終了フラグ
}
reset(loop = false){
this.loop = loop;
this.index = 0;
if(this.length > 0){
this.returnable = true;
}
}
pick(){
if(this.length === 0){ return null; }
const L = this.length;
if(this.index >= L){
return null;
}
const v = this[this.index];
this.index++;
if(this.index >= L){
if(this.loop){
// ここはloop前提のため、引数にtrueを指定しなければならない。
// 止まってしまう。事前に気づけて良かった。
this.reset(true);
}else{
// loopでないなら終了した場合にフラグを折る
this.returnable = false;
}
}
return v;
}
isReturnable(){
return this.returnable;
}
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
}
// RandomChoiceArray.
// 通常の配列と同じように作れる。fromやtoでも作れる。
// resetでランダムindex配列が生成されそれに従って順繰りに取られていく
// resetの際にtrueを指定すると際限なくランダム値を出し続ける
class RandomChoiceArray extends ArrayWrapper{
constructor(){
super(...arguments);
this.loop = false;
this.indices = [];
this.returnable = false; // 終了フラグ
}
reset(loop = false){
this.loop = loop;
this.indices.length = 0;
const L = this.length;
if(L === 0) return;
// 長さが1以上の場合にフラグを立てる
this.returnable = true;
// 雑にシャッフル。普通にやるわ。
const src = Array.from(".".repeat(L), (x, i) => { return {value:i, seed:Math.random()}; });
src.sort((a, b) => a.seed - b.seed);
this.indices = src.map(u => u.value);
}
pick(){
if(this.length === 0){ return null; }
if(this.indices.length === 0){
return null;
}
const v = this[this.indices.pop()];
if(this.indices.length === 0){
if(this.loop){
// ここはloop前提のため、引数にtrueを指定しなければならない。
// 止まってしまう。事前に気づけて良かった。
this.reset(true);
}else{
// loopでないなら終了した場合にフラグを折る
this.returnable = false;
}
}
return v;
}
isReturnable(){
return this.returnable;
}
static from(){
return super.from(...arguments);
}
static of(){
return super.of(...arguments);
}
}
returnableはpick直後に、それによって最後である場合にfalseとなります。なのでチェックするタイミングはpick()の直前です。直前のisReturnable()のチェックでもしtrueであればその値は(nullであっても)配列内の値であることが保証されます。falseであるならば、そのあとのpick()の結果は「否定のnull」であり、配列内のnullではないということです。つまり中身を限界まで走査したいのであれば、事前に判定してtrueならチェックに使う、falseなら使わずに次の処理に行く、という形になるかと思います。
もちろん配列内にnullが無いことの保証がされているなら、nullかどうか見るだけで足ります。いろんな選択肢があるのはいいことです。