プログラミング はじめの一歩 JavaScript + p5.js編
22:オウムが話す言葉

この記事の詳しい内容には毎日新聞の「オウムが話す言葉」ページから有料記事に進むことで読めます。

概要

オウムのロボット1号と2号がいます。オウムロボットは手順の通りに言葉を覚えていきますが、覚えられるのは1つだけで、新しく覚えると、前に覚えた言葉は忘れてしまいます。また、ロボットがロボットに言葉を教えることもできます。

手順:

  1. オウム1号 <- 'くり'
  2. オウム2号 <- 'はくさい'
  3. オウム1号 <- オウム2号

手順1は、「オウム1号に言葉’くり’を覚えさせる」という意味で、2は「オウム2号に言葉’はくさい’を覚えさせる」という意味です。手順3は「オウム2号が今覚えている言葉をオウム1号に覚えさせる」という意味です。

手順1でオウム1号は’くり’を覚え、手順2でオウム2号は’はくさい’を覚えます。そして手順3で、オウム2号が今覚えている言葉’はくさい’を、オウム1号に教えるので、最終的にオウム1号と2号の覚えている言葉は’はくさい’になります。

ここで問題です。次の手順でオウムロボットに言葉を覚えさせた場合、最終的にオウム2号が覚えている言葉何でしょう?
手順:

  1. オウム1号 <- 'みかん'
  2. オウム2号 <- 'りんご'
  3. オウム2号 <- オウム1号
  4. オウム1号 <- 'かき'
論理を考える

問題を、頭の中だけで考えると少し混乱するかもしれませんが、メモを取りながら考えると、’みかん’であることは容易に分かります。

実はこれとよく似たお題は「4:言葉を覚えるロボット」ですでに取り上げています。そのときはロボット用の変数を使って問題を解決しました。

変数を使う

プログラムではほぼ例外なく変数を使うので、この「変数を使う」というのも妙ですが、変数がオウムロボットのメモリ(記憶装置)だと考えてください。このメモリは容量がないので1つしか覚えられません。

メモリに新しい言葉を覚えさせると、前に覚えていた言葉は上書きされ失われます。JavaScriptで上書きできる変数はletキーワードで作成できます。次のコードでは、オウム1号のメモリを変数o1、2号のメモリを変数o2としています。

// 変数を使う 最初は何も覚えていない
let o1 = ''; // オウム1号
let o2 = ''; // オウム2号

// オウム1号 <- 'くり' オウム1号に'くり'を覚えさせる
// => 変数o1に'くり'を代入する
o1 = 'くり';
print(o1); // くり
// オウム2号 <- 'はくさい' オウム2号に'はくさい'を覚えさせる
o2 = 'はくさい';
print(o2); // はくさい

// オウム1号 <- オウム2号 オウム1号にオウム2号が覚えている言葉を覚えさせる
// => o2の値をo1に代入する
o1 = o2;
print(o1); // はくさい

同様に、問題を解決するコードも次のように記述できます。

// 変数を使う 最初は何も覚えていない
let o1 = ''; // オウム1号
let o2 = ''; // オウム2号
// オウム1号 <- 'みかん'
o1 = 'みかん';
// オウム2号 <- 'りんご'
o2 = 'りんご';
// オウム2号 <- オウム1号
o2 = o1;
// オウム1号 <- 'かき'
o1 = 'かき';
print(o2); // みかん

o2 = o1 というのは、o1が参照している値をo2に代入する、という意味です。変数や値といったプログラミング用語の意味については「読み取り、評価、出力、繰り返し」が参考になります。

OOPで考える

オブジェクト指向プログラミング(OOP)の手法は、ロボットに置き換えると理解しやすくなります。以降では、お題をオウムロボットのOOPプログラムで解決する方法を見ていきます。

オウムロボットに必要なもの
OOPでは、今の場合ならオウムロボットをクラスで表します。クラスにはプロパティと呼ばれる特性(属性)と、メソッドと呼ばれるアクションを定めます。

オウムロボットのクラスを作成すると、そこから1号や2号、3号が作成できます。クラスは1号や2号を作り出す元になるもので、1号や2号は言わばクラスのコピーです。しかしプロパティを変えることで1号と2号を違うオウムロボットにすることができます。

ではオウムロボットに必要なものは何でしょうか? 言うまでもなく1語だけ覚えておけるメモリです。メソッドはこのメモリに言葉を設定したり、今の言葉を取り出したりするアクションを作成します。

下記はオウムロボットのクラスの例です。メモリはwordという名前のプロパティで表しています。

// オウムロボットのクラス
class OumuRobot {
    constructor(name) {
            // オウムロボットに必要な特性
            this.name = name; // ほかと区別するための名前
            this.word = ''; // 言葉を1つだけ覚える入れ物
        }
        // 言葉を聞いて覚える => 送られてきたwをwordプロパティに代入する
    listen(w) {
            this.word = w;
        }
        // 今覚えている言葉を調べる
    getWord() {
            return this.word;
        }
        // しゃべる => nameとwordプロパティを出力する
    speech() {
        print(this.name + ': ' + this.word);
    }
}

constructor(コンストラクタ)はクラスの特別なメソッドで、クラスからオブジェクト(インスタンス)を作成するとき、1度だけ自動的に実行されます。ここではthisキーワードを使ってプロパティを設定しています。nameプロパティはオウムロボットの名前です。

メソッドは多くの場合、プロパティに変化を与えます。listen()メソッドは引数として受け取ったwをwordプロパティに代入します。これは「オウムロボットが言葉を聞いて覚える」といった働きを持ちます。getWord()メソッドは今覚えている言葉を返します。speech()はプロパティの値を出力して確認するためのメソッドです。

クラスはsetup()関数内でインスタンス化できます。具体的に言うと、OumuRobotクラスからオウム1号やオウム2号を表すオブジェクトが作成できます。オウム1号とオウム2号は同じOumuRobotクラスから作成されますが、名前(nameプロパティ)が違います。

function setup() {
    noCanvas();
    // OumuRobotクラスのインスタンスを2つ作成
    const o1 = new OumuRobot('オウム1号');
    const o2 = new OumuRobot('オウム2号');

    // オウム1号 <- 'くり' オウム1号に'くり'を覚えさせる
    o1.listen('くり');
    o1.speech();
    // オウム2号 <- 'はくさい' オウム2号に'はくさい'を覚えさせる
    o2.listen('はくさい');
    o2.speech();
    // オウム1号 <- オウム2号 オウム2号の覚えている言葉をオウム1号に覚えさせる
    // => オウム2号のwordプロパティを調べそれをオウム1号に覚えさせる
    o1.listen(o2.getWord());
    o1.speech();
}

OumuRobotクラスのインスタンスに言葉を覚えさせるにはlisten()メソッドを使用します。listen()メソッドに覚えさせたい言葉の文字列を指定して、インスタンスから呼び出すと、そのインスタンスのwordプロパティに言葉の文字列が割り当てられます。o1とo2は同じOumuRobotクラスから作成していますが、wordプロパティに異なる値を持つことができます。これは、オウム1号とオウム2号は自分だけの特性を持てるということです。

o1.listen(o2.getWord()); の行は少し複雑ですが、分けて考えると理解できます。o2.getWord()はo2が今覚えている言葉を返すメソッドです。o2が今覚えているのは’はくさい’なので、o2.getWord()は’はくさい’を返します。ということは、o1.listen(o2.getWord())はo1.listen(‘はくさい’)ということなので、o1は’はくさい’を覚えます。このときo1が前に覚えていた’くり’は上書きされ、o1のwordプロパティの値は’はくさい’に変わります。

下図はこのプログラムの出力結果を示しています。

OOPの手法を使ってオウム1号とオウム2号に言葉を覚えさせる論理は以上です。次は可視化していきましょう。

可視化

オウム1号とオウム2号は同じOumuRobotのオブジェクトですが、異なるインスタンスなので、自分の値(プロパティ値)を持つことができます。したがって同じメソッドでも、自分のプロパティを扱う限り、そのオウム固有のアクションが実行できます。

イメージも同様で、オウム1号とオウム2号に固有のイメージを与えることができます。位置を表すxとyプロパティを定義すると、別々の位置に描画できます。

描画には次のPNGファイルを使用します。オウムの絵の上にあるのは吹き出しで、オウムの絵と一体化しています。オウムのイメージを描画してから、この吹き出しの位置に文字を描画すると、吹き出し文字を描画しているように見えます。

OumuRobotクラスの修正

OumuRobotクラスは次のように変更します。コンストラクタのパラメータには位置のxとy、イメージのimgプロパティを追加しています。display()メソッドではこれらを使ってオウムのイメージを描画し、またtext()関数でwordプロパティを描画します。

class OumuRobot {
    constructor(name, x, y, img) {
        this.name = name;
        this.word = '';
        this.x = x; // このインスタンス固有のx位置
        this.y = y; // このインスタンス固有のy位置
        this.image = img; // このインスタンス固有のイメージ
    }

    // イメージと吹き出し文字の描画
    display() {
        image(this.image, this.x, this.y);
        text(this.word, this.x - this.image.width / 2 + 40, this.y - this.image.height / 2 + 20);
    }
    listen(w) {
        this.word = w;
    }
    getWord() {
        return this.word;
    }
    speech() {
        print(this.name + ': ' + this.word);
    }
}

メインのsketch.jsではpreload()関数でイメージをロードし、setup()関数でOumuRobotクラスのインスタンスo1とo2を作成します。

newキーワードにクラス名とかっこをつづけると、そのクラスのコンストラクタメソッドが呼び出されます。コンストラクタにはx,y,imgパラメータを追加したので、その値としてオウム1号の(x,y)位置とイメージ、オウム2号の(x,y)位置とイメージを指定します。

すると、オウム1号を表すOumuRobotクラスのインスタンスとオウム2号を表すOumuRobotクラスのインスタンスが作成され返されるので、これを変数o1とo2に割り当てます。

実行と結果

インスタンスを作成すると、それを割り当てた変数を使って個別にメソッドを呼び出すことができます。

let o1, o2;
let o1Image, o2Image;

function preload() {
    o1Image = loadImage('images/oumu1.png');
    o2Image = loadImage('images/oumu2.png');
}

function setup() {
    createCanvas(300, 200);
    imageMode(CENTER);
    textSize(13);
    textAlign(CENTER);
    // OumuRobotクラスのインスタンスを2つ作成
    o1 = new OumuRobot('オウム1号', 110, 100, o1Image);
    o2 = new OumuRobot('オウム2号', 180, 101, o2Image);
    ...
}

下記は問題の手順を実行するコードです。

o1.listen('みかん');
o1.speech();
o2.listen('りんご');
o2.speech();
o2.listen(o1.getWord());
o2.speech();
o1.listen('かき');
o1.speech();

コードを実行するすると、下図の結果が表示されます。ここからオウム2号が覚えている言葉は’みかん’であることが分かります。

HTML要素を使ってインタラクティブにする

上記のo1.listen(‘みかん’)、o2.listen(‘りんご’)、o2.listen(o1.getWord())といった行は一気に実行されるので、少し面白みに欠けます。クリックなどのユーザー操作を使って、1つずつ実行するように変えると、wordプロパティが上書きされるのが実感できます。

次のコードではHTMLのリストを作成して、そのクリックでlisten()メソッドを呼び出すようにしています。

// HTMLのリストを作成
// <ul>要素
const ul = createElement('ul');

// <li>要素
const li1 = createElement('ul', 'オウム1号 <- みかん');
const li2 = createElement('ul', 'オウム2号 <- りんご');
const li3 = createElement('ul', 'オウム2号 <- オウム1号');
const li4 = createElement('ul', 'オウム1号 <- かき');
// <li>要素を配列にまとめ、
let list = [li1, li2, li3, li4];
// 一気に設定
for (let i = 0; i < list.length; i++) {
    // <li>要素のCSSスタイルを設定
    list[i].style('border', 'solid black thin'); // 枠線
    list[i].style('width', '200px'); // 幅
    list[i].style('cursor', 'pointer'); // カーソル
    // <li>要素の親は<ul>要素
    ul.child(list[i]);
}
ul.position(0, 200);
// <ul>要素がクリックされたら
ul.elt.onclick = (e) => {
    // 子要素を全部取得
    const childs = ul.elt.childNodes;
    // このインデックス番号が知りたい
    let index;
    // 子要素を走査して
    for (index = 0; index < childs.length; index++) {
        // クリックされた要素(<li>要素)と、走査中の子要素が一致したらそこで終了
        if (e.target === childs[index]) {
            break;
        }
    }
    // インデックス番号が0なら0番めの<li>要素がクリックされたということ、
    if (index === 0) {
        o1.listen('みかん');
    }
    // インデックス番号が1なら1番めの<li>要素がクリックされたということ、
    else if (index === 1) {
        o2.listen('りんご');
    }
    // インデックス番号が2なら1番めの<li>要素がクリックされたということ、
    else if (index === 2) {
        o2.listen(o1.getWord());
    }
    // インデックス番号が3なら3番めの<li>要素がクリックされたということ、
    else if (index === 3) {
        o1.listen('かき');
    }
}
function draw() {
    background(225, 234, 187);
    o1.display();
    o2.display();
}

次はこの実行結果です。リストのクリックでオウムロボットが言葉を覚えます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA