この記事の詳しい内容には毎日新聞の「パンの入れ物はどれ?」ページから有料記事に進むことで読めます。
目次
概要
舞台はとあるパン屋さんです。このパン屋さんでは、焼きたてのパンは紙のふくろに入れて渡しますが、焼きたてでないパンは、チョコを使っていたら箱に入れ、そうでないパンはレジ袋に入れて渡します。
たとえば、焼きたてのクリームパンは紙のふくろに入れ(下図の(1))、焼きたてでないチョコパンは箱に入れて(同(2))渡します。また焼きたてでないあんぱんはレジ袋に入れて(同(3))渡します。
そこで問題です。焼きたてのチョコパンはどれに入れるでしょう?
論理を考える
多くの参考書などでは、最短距離で到達する正解が示されているだけですが、通常は解決の糸口を探る試行錯誤が必要です。試行錯誤はときにはまったくの的外れの場合もありますが、それは結果としてそうであっただけで、解決に近づく方法としては間違っていません。うまくいかないことが分かったことが成果であり、経験となります。
本稿では、解決の糸口を探るために取られそうな試行錯誤を追っていくことにします。
試行錯誤の実践
冒頭のお題を見て、多くの方は、「これはif文で解決できるな」と予想されるでしょう。そこで次のような疑似コードを思い浮かべられるでしょう。
もしパンが焼きたてなら、
紙のふくろに入れて渡す
そうでなくもしパンが焼きたてでないなら
もしパンがチョコを含んでいるなら
箱に入れて渡す
そうでないなら
レジ袋に入れて渡す
早速これをコードにします。パンはとりあえず、「1:おかしに当たり券を入れる」の「おかしは文字で表す」で述べているように、文字で表すことにします。
let container;
let bread = '焼きたてのクリームパン';
if (bread === '焼きたてのクリームパン') {
container = '紙のふくろ';
}
else {
if (bread === 'チョコを含んでいる') {
container = '箱';
}
else {
container = 'レジ袋';
}
}
print(container);
これを実行すると、[コンソール]に”紙のふくろ”が表示されます。これは”焼きたてのクリームパン”に対する正しい結果です。
つづいてbreadを’焼きたてでないチョコパン’に置き換えます。[コンソール]には”レジ袋”が表示されます。この結果は間違っています。これはコードを書いた本人も気づいているでしょう。breadが’焼きたてのクリームパン’という文字列でなく、’チョコを含んでいる’という文字列でもないので、containerは’レジ袋’になります。
そこで考えます。「チョコを含んでいる」とはどういうことか? 本物のパンなら、パンにチョコが使われていることだが、今パンは文字列で表している。….. !! そうか、breadに’チョコ’という文字列が含まれているということなのではないか!
そこでインターネットで検索して調べます。するとStringオブジェクトのindexOf()というメソッドが見つかります。
indexOf() メソッドは、呼び出す String オブジェクト中で、 fromIndex から検索を始め、指定された値が最初に現れたインデックスを返します。値が見つからない場合は -1 を返します。
breadが’チョコ’を含んでいることは、bread.indexOf(‘チョコ’) !== -1 で調べられるので、次のようなコードが記述できます。
let container;
let bread = '焼きたてのクリームパン';
bread = '焼きたてでないチョコパン';
bread = '焼きたてでないあんぱん';
if (bread === '焼きたてのクリームパン') {
container = '紙のふくろ';
}
else {
// if (bread === 'チョコを含んでいる') {
if (bread.indexOf('チョコ') !== -1) {
container = '箱';
}
else {
container = 'レジ袋';
}
}
print(container);
このコードは、breadが’焼きたてのクリームパン’のときも’焼きたてでないチョコパン’のときも、そして’焼きたてでないあんぱん’のときもうまくいきます。しかし最後の’焼きたてのチョコパン’で’箱’が表示されます。焼きたての場合はどのパンも’紙のふくろ’なので、この結果は誤りです。
では間違いの原因はどこにあるのか? そもそも最初のif文ではbreadを’焼きたてのクリームパン’と比べているので、ここで’焼きたてでないチョコパン’がはじかれるのは当然です。そこで前と同じようにindexOf()を使うことにします。
let container;
let bread = '焼きたてのクリームパン';
bread = '焼きたてでないチョコパン';
bread = '焼きたてでないあんぱん';
bread = '焼きたてのチョコパン';
if (bread.indexOf('焼きたての') !== -1) {
container = '紙のふくろ';
}
else {
if (bread.indexOf('チョコ') !== -1) {
container = '箱';
}
else {
container = 'レジ袋';
}
}
print(container);
焼きたてか焼きたてでないかは、’焼きたて’でなく’焼きたての’で調べる必要があります。「焼きたては焼きたてだろう、なんで”の”まで含めるんだ、面倒くさい」と思われるでしょうが、JavaScriptにはただの文字列であり、温かいとかふかふかした焼きたてのイメージはまったく関係しません。
本稿のお題はこの論理で解決できます。しかし…。もし’焼きたてのチョコパン’でなく、’焼き立てのチョコパン’にすると、breadに’焼きたての’が含まれていず、’チョコ’が含まれているので、結果は’箱’になります。もしこのプログラムをこのパン屋さんで使うことになり、”焼きたて”を”焼き立て”で入力したら、このプログラムは間違いを出力することになります。breadの比較に文字列を使っているプログラマーの思慮の浅さだと言われてもしかたないところです。
やり直し
そこで最初からやり直すことにします。残念ですがしかたありません。String.indexOf()をいうメソッドを覚えたのでよしとしましょう。
冒頭の例で示されているパンについて、焼きたてかどうかとチョコが使われているかどうかで分けると、下図のようになります。
チョコパンもあんぱんもクリームパンもどれもパンです。ここでこれらを区別しているのは、焼きたてかどうかとチョコが使われているかどうかというパンの状態です。
ここで、前の「おかしの文字列をオブジェクトに置き換える」のように、焼きたてかどうかとチョコが使われているかどうかを特性(属性、プロパティ)として持つBreadオブジェクトを考えます。焼きたてかどうかをisHot、チョコかどうかをisChocoで表すと、焼きたてのチョコパンは、下図のように表すことができます。
ほかも同様です。そしてこれらはそれぞれが入れ物の種類に対応します。
このパンは次のクラスで表すことができます。typeは’焼きたてのチョコパン’などの文字列を参照するプロパティとして設けたものです。
// パンのクラス
// typeはパンの特性を表した名前、isHotは焼きたてかどうか、isChocoはチョコを含むかどうか
class Bread {
constructor(type, isHot, isChoco) {
this.type = type;
this.isHot = isHot;
this.isChoco = isChoco;
}
}
“焼きたてのチョコパン”は、焼きたてでチョコが使われているので、次のコードで作成できます。
const hotChoco = new Bread('焼きたてのチョコパン', true, true);
パンに対して入れ物を決める論理は、前の文字列のものと同じですが、比較の相手が文字列でなく、BreadオブジェクトのisHotとisChocoに変わります。4つのパンに対して実行するので、関数で定義すると、次のような関数が記述できます。パラメータはBreadオブジェクトです。
// 受け取ったBreadオブジェクトの特性を調べ、ルールに合った入れ物を返す
function whichContainer(bread) {
let container; // 入れ物
// Breadオブジェクトの特性(3つのプロパティ)を取り出す
const type = bread.type;
const isHot = bread.isHot;
const isChoco = bread.isChoco;
print(type + 'を調べる');
// 焼きたてなら
if (isHot) {
// 入れ物は紙のふくろ
container = '紙のふくろ';
// 焼きたてでないなら
}
else {
// チョコを含んでいるなら
if (isChoco) {
// 入れ物は箱
container = '箱';
// チョコを含んでいないなら
}
else {
// 入れ物はレジ袋
container = 'レジ袋';
}
}
// 呼び出し元にルールに合った入れ物を返す
return container;
}
whichContainer()関数は次のように使用します。
// Breadクラスに3つの特性を渡してBreadインスタンスを作成する
const hotCream = new Bread('焼きたてのクリームパン', true, false);
// BreadインスタンスをwhichContainer()に渡して、ルールに合った入れ物を得る
let container = whichContainer(hotCream);
print('-> ' + container);
const notHotChoco = new Bread('焼きたてでないチョコパン', false, true);
container = whichContainer(notHotChoco);
print('-> ' + container);
const notHotAn = new Bread('焼きたてでないあんぱん', false, false);
container = whichContainer(notHotAn);
print('-> ' + container);
const hotChoco = new Bread('焼きたてのチョコパン', true, true);
container = whichContainer(hotChoco);
print('-> ' + container);
[コンソール]には次の結果が表示されます。
やり直しから分かるのは、パンのような同じようなものはオブジェクトとして作成する方が分かりやすくかつ応用が効くということです。文字列は文字そのものを表すだけで融通性がないので、次への進展が望めません。
可視化
では最後に可視化したコードを示しておきます。パンの選択には<select>要素を使っています。
// Breadインスタンスを入れる配列
let breads;
// パンと入れ物のイメージを入れる配列
let breadImages, containerImages;
// 現在のパンのイメージ、現在の入れ物のイメージ
let currentBreadImage, currentContainerImage;
// 画面に表示する文字
let msg;
// パンと入れ物のイメージを読み込み、配列に入れる
function preload() {
const dummyImage = loadImage('images/dummy.png');
const creamImage = loadImage('images/cream.png');
const chocoImage = loadImage('images/choco.png');
const anImage = loadImage('images/an.png');
breadImages = [dummyImage, creamImage, chocoImage, anImage];
const boxImage = loadImage('images/box.png');
const paperBagImage = loadImage('images/paperBag.png');
const shoppingBagImage = loadImage('images/shoppingBag.png');
containerImages = [dummyImage, boxImage, paperBagImage, shoppingBagImage];
}
function setup() {
createCanvas(400, 200);
noStroke();
textSize(15);
// Breadインスタンスを作成し、配列に入れる
const dummy = new Bread('パンを選ぶ', false, false, breadImages[0]);
const hotCream = new Bread('焼きたてのクリームパン', true, false, breadImages[1]);
const notHotChoco = new Bread('焼きたてでないチョコパン', false, true, breadImages[2]);
const notHotAn = new Bread('焼きたてでないあんぱん', false, false, breadImages[3]);
const hotChoco = new Bread('焼きたてのチョコパン', true, true, breadImages[2]);
breads = [dummy, hotCream, notHotChoco, notHotAn, hotChoco];
// <select>要素の作成
const select = createSelect();
select.position(10, 10);
// 選択肢を設定
for (let i = 0; i < breads.length; i++) {
select.option(breads[i].type, i);
}
select.selected(breads[0]);
// 選択肢に変化があったら
select.changed(() => {
// 選択した値(0から4)
const val = int(select.value());
// 'パンを選ぶ'が選ばれたときはこの関数を抜ける
if (val === 0) return;
// 選択されたBreadインスタンスを特定
const bread = breads[val];
// whichContainer()関数に渡してルールに合った入れ物を得る
const container = whichContainer(bread);
// テキストで描画
msg = bread.type + 'の入れ物は' + container + 'です。';
// パンのイメージを描画
currentBreadImage = bread.image
});
// 最初はダミー(透明のpng)を描画する
currentBreadImage = breadImages[0];
currentContainerImage = containerImages[0];
}
function draw() {
background(235, 239, 171);
// 中央付近の背景を白にする
fill(255);
rect(100, 100, 200, 70);
image(currentBreadImage, 120, 110);
image(currentContainerImage, 250, 110);
// 文字を黒で描画
fill(0);
text(msg, 20, 50);
}
function whichContainer(bread) {
let container;
const type = bread.type;
const isHot = bread.isHot;
const isChoco = bread.isChoco;
print(type + 'を調べる');
if (isHot) {
container = '紙のふくろ';
currentContainerImage = containerImages[2];
}
else {
if (isChoco) {
container = '箱';
currentContainerImage = containerImages[1];
}
else {
container = 'レジ袋';
currentContainerImage = containerImages[3];
}
}
return container;
}
class Bread {
constructor(type, isHot, isChoco, img) {
this.type = type;
this.isHot = isHot;
this.isChoco = isChoco;
this.image = img;
}
}
下はこの実行画面です。