[EffectiveJavaScript輪読会7]列挙の実行中にオブジェクトを変更しない

GAS

どうも。つじけ(tsujikenzo)です。このシリーズでは2021年8月からスタートしました「ノンプロ研EffectiveJavaScript輪読会」についてお送りします。

前回のシリーズでは、第5章の考察をお届けしました。

引き続き、第5章をお送りします。いよいよ後2回で終わりです。

目次と日程

第5章後半は「配列とディクショナリ」です。組み込みオブジェクトのArrayオブジェクトを操作するのかと思いきや、引き続きプロトタイプの考察です。

  • 第1章 JavaScriptに慣れ親しむ
  • 第2章 変数のスコープ
  • 第3章 関数の扱い
  • 第4章 オブジェクトとプロトタイプ
  • 第5章 配列とディクショナリ
  • 第6章 ライブラリとAPI設計
  • 第7章 並行処理

LT大会は2021年11月28日です。がんばりましょう。

今日はさっそく1回目で、 「列挙の実行中にオブジェクトを変更しない」 をお届けします。

テキスト第5章「配列とディクショナリ」の項目48に対応しています。

今日のアジェンダ

  • for in文の中で列挙を変更する
  • WorkSetクラスを追加する
  • Mapオブジェクトでリファクタリング

for in文の中で列挙を変更する

以下のような、友だちのネットワークがあるとします。

そして、それぞれのPersonについて調べたときに、ネットワーク内にいるかどうかというメソッドを考えていきます。 

Georgeは、誰ともつながっていないので、ネットワーク内にいるかどうかのinNetwork()メソッドで、falseを返すはずです。

function myFunction7_48_01() {

  const a = new Member('Alice');
  const b = new Member('Bob');
  const c = new Member('Carol');
  const d = new Member('Dieter');
  const e = new Member('Eli');
  const f = new Member('Fatima');
  const g = new Member('George');

  a.friends.push(b);
  b.friends.push(c);
  c.friends.push(e);
  d.friends.push(b);
  e.friends.push(d, f);
 
  console.log(a.inNetwork(g)); //false

}


class Member {

  constructor(name) {
    this.name = name;
    this.friends = [];
  }

  inNetwork(other) {

    const visited = {};
    const workset = {};

    workset[this.name] = this;

    for (const name in workset) {

      const member = workset[name];
      delete workset[name];

      if (name in visited) continue;

      visited[name] = member;

      if (member === other) return true;

      member.friends.forEach(friend => workset[friend.name] = friend);

    }
    return false;
  }

}

しかし、本来は、ネットワーク内にいるはずの、AliceとFatimaの関係性も、falseを返してしまいます。

console.log(a.inNetwork(f)); //false

これは、for in文のループ内では、オブジェクトの列挙の変更(更新)が要求されない、という仕様によるものです。

上記のコードでは、for in文の中で、delete文による要素の削除を要求したのが原因です。

delete workset[name];

この問題を解決してみましょう。

WorkSetクラスを追加する

項目45で紹介されていた、Dictクラスは、Mapオブジェクトに読み替えてGAS化します。

まず、WorkSetクラスのプロパティを、Mapオブジェクトに差し替えます。

pick()メソッドも追加しています。

そして、完成したコードがこちらです。

function myFunction7_48_02() {

  class Member {

    constructor(name) {
      this.name = name;
      this.friends = [];
    }

    inNetwork(other) {

      const visited = {};
      const workset = new WorkSet();
      workset.add(this.name, this);

      while (!workset.isEmpty()) {

        const name = workset.pick();
        const member = workset.get(name);
        workset.remove(name);

        if (name in visited) continue; //同じメンバーを再訪問しない

        visited[name] = member;

        if (member === other) return true; //見つかった?

        member.friends.forEach(friend => workset.add(friend.name, friend));

      }
      return false;
    }

  }


  class WorkSet {

    constructor() {
      this.entries = new Map();
      this.count = 0;
    }

    isEmpty() {
      return this.count === 0;
    }

    add(key, val) {
      if (this.entries.has(key)) return;
      this.entries.set(key, val);
      this.count++;
    }

    get(key) {
      return this.entries.get(key);
    }

    remove(key) {
      if (!this.entries.has(key)) return;
      this.entries.delete(key);
      this.count--;
    }

    pick() {
      const keys = [...this.entries.keys()];
      if (keys.length) return keys[0];
      throw new Error('empty dictionary');
    }

  }


  const a = new Member('Alice');
  const b = new Member('Bob');
  const c = new Member('Carol');
  const d = new Member('Dieter');
  const e = new Member('Eli');
  const f = new Member('Fatima');
  const g = new Member('George');

  a.friends.push(b);
  b.friends.push(c);
  c.friends.push(e);
  d.friends.push(b);
  e.friends.push(d, f);

  console.log(a.inNetwork(g)); //false
  console.log(a.inNetwork(f)); //true

}

Mapオブジェクトでリファクタリング

Mapオブジェクトの復習をしましょう。

[GAS]連想配列はMapオブジェクトを使おう 前半
どうも。つじけ(tsujikenzo)です。このシリーズでは「連想配列はMapオブジェクトを使おう」を、前半、後半でお送りします。今日は、前半戦です。今日のアジェンダはじめにMapオブジェクトの生成Mapオブジェクトの主な...

WorkSetクラスのメンバーは、ほぼ、Mapオブジェクトのメンバーです。

と、いうことは、inNetWork()メソッドの問題は、Mapオブジェクトで解決できそうです。

function myFunction7_48_03() {

  class Member {
    constructor(name) {
      this.name = name;
      this.friends = [];
    }

    inNetwork(other) {
      const visited = {};
      const workset = new Map([[this.name, this]]);

      while (workset.size) {

        const name = [...workset.keys()][0]; // pick メソッドを代⽤したステートメント
        const member = workset.get(name);

        workset.delete(name);

        if (name in visited) continue;

        visited[name] = member;

        if (member === other) return true;
        
        member.friends.forEach(friend => workset.set(friend.name, friend));
      }
      return false;
    }
  }

  const a = new Member('Alice');
  const b = new Member('Bob');
  const c = new Member('Carol');
  const d = new Member('Dieter');
  const e = new Member('Eli');
  const f = new Member('Fatima');
  const g = new Member('George');

  a.friends.push(b);
  b.friends.push(c);
  c.friends.push(e);
  d.friends.push(b);
  e.friends.push(d, f);

  console.log(a.inNetwork(g)); //false
  console.log(a.inNetwork(f)); //true

}

最後に、visitedsを配列にしたバージョンがこちらです。

function myFunction7_48_04() {

  class Member {

    constructor(name) {
      this.name = name;
      this.friends = [];
    }

    inNetwork(other) {
      const visiteds = [];
      const workset = new Map([[this.name, this]]);

      while (workset.size) {

        const name = [...workset.keys()][0]; // pick メソッドを代⽤
        const member = workset.get(name);

        workset.delete(name);

        if (visiteds.map(visited => visited.name).includes(member.name)) continue;
       
        visiteds.push(member);

        if (member === other) return true;

        member.friends.forEach(friend => workset.set(friend.name, friend));
      }

      return false;
    }

  }

  
  const a = new Member('Alice');
  const b = new Member('Bob');
  const c = new Member('Carol');
  const d = new Member('Dieter');
  const e = new Member('Eli');
  const f = new Member('Fatima');
  const g = new Member('George');

  a.friends.push(b);
  b.friends.push(c);
  c.friends.push(e);
  d.friends.push(b);
  e.friends.push(d, f);

  console.log(a.inNetwork(g)); //false
  console.log(a.inNetwork(f)); //true

}

まとめ

以上で、「列挙の実行中にオブジェクトを変更しない」をお届けしました。

次回は、「配列の反復処理には、for…inループではなく、forループを使おう」と、「配列コンストラクタよりも配列リテラルのほうが好ましい」 を、2本まとめてお届けします。

Special Thanks

etauさん

このシリーズの目次

タイトルとURLをコピーしました