2021/09/30

ヒューマン・リソース・マシーン 入社6年目−真夏の雨

 目次

1)課題

左側のコンベアのパネルを2つごとに加算した結果を、右側に運んでください。
[ADD]コマンドが使用可能になりました。指定箇所の数値を、手元の数値に加算します。

2)解説

足し算をするADDコマンドの使い方を学ぶための問題です.

コマンドをいくつか並べて, ADDコマンドの動作を確認します. ステップ実行して, ADDコマンドの動作を確認してみましょう. 

COPYTOコマンドまで実行しました. カーペットの0番に8と書いたパネルを置いています.

INBOXコマンドを実行したので, 左のコンベアから3と書いたパネルを取りました.

ADDコマンドを実行しました.  ADDコマンドの左の数字は0となっています. ADDコマンドを実行する前は, カーペットの0番には8と書いたパネルが置いてあり, 3と書いたパネルを持っていました. ADDコマンドを実行すると, 11と書いたパネルを持っています. つまり, ADDコマンドは, カーペットのパネルと持っているパネルの数字を足し, 足した結果を書いたパネルを持ちます. また, カーペット0番に置いてあるパネルの数字は変わっていないことも確認してください.

このコマンドの並び, 即ちコードによって, 左のコンベアのパネル2枚を加算できました. 持っているパネルを右のコンベアに運べば, 課題をクリアーできます.

ADDコマンドの動作を確認するコードは, 左のコンベアからパネルを2枚取り出し, 加算した結果を持っています. 加算した結果をそのまま右のコンベアに運び, JUMPコマンドで先頭に戻れば, 課題をクリアーできます.

3)補足 

ADDコマンドがエラーとなり, 上司に怒られる条件を確認します.

パネルを持たず, カーペットにもパネルを置かずにADDコマンドを実行すると上司に怒られます.

パネルを持っていますが, カーペットにパネルを置かずにADDコマンドを実行しても上司に怒られます.

カーペットにパネルを置いていますが, パネルを持たずにADDコマンドを実行しても上司に怒られます.

4)まとめ

  1. この課題では, ADDコマンドを使ってカーペットのパネルと持っているパネルを加算する方法を確認しました.
  2. 加算した結果は持っているパネルに反映されます. 一方, カーペットのパネルは変化しません. 
  3. ADDコマンドを実行する時は, パネルを持っていることと, カーペットにパネルを置いていることの両方の条件を満たす必要があります.
  4. ADDコマンドを実行できる条件を満たしていないのに, ADDコマンドを実行すると上司に怒られます. 上司はすぐに怒るので, 慎重にコードを組み立てましょう.

2021/09/28

ヒューマン・リソース・マシーン 入社4年目−逆にして運んで

目次

1)課題

左側のコンベアのパネルが無くなるまで、右側に運んでください。運ぶ時はパネルが2つごとに逆になるようにしてください。
[COPYTO]コマンドが使用可能になりました。データのコピー品を、カーペットの指定した場所に置くことが出来ます。
問題文がわかりにくい場合はそこにいる上司に解答例を聞いてみましょう。

2)解説

COPYTOコマンドはCOPYFROMコマンドの対となるコマンドです. COPYFROMコマンドと同じようにCOPYTOコマンドにもカーペットの位置を示す番号が付きます. 

まず, COPYTOコマンドの動作を確認してみます. INBOXコマンドとCOPYTOコマンドを実行してみました. 左のコンベアからパネルを取り, カーペットの0番にパネルのコピーを置いています. カーペットに置くのはパネルのコピーであり, 左のコンベアから取ったパネルをそのまま持っていることに注意してください.

ここで解答例を見て, 上司が要求していることを確認します. 先頭2枚のパネルだけを考えます. 左のコンベアに4, 9とパネルが並んでいるなら, 右のコンベアに9, 4の順にパネルを運べば上司の求めに応じられます. 

上司の要求に応えるための手順を整理してみます.

  1. 左のコンベアからパネルを取りだす.
  2. カーペットの0番にパネルのコピーを置く.
  3. 左のコンベアからパネルを取りだす.
  4. そのままパネルを右のコンベアに運ぶ.
  5. カーペットの0番に置いたパネルを取りだす.
  6. 右のコンベアに運ぶ.

この手順通りにコマンドを並べてみます.

この内容で実行してみます. 上司に怒られてしまいましたが, 左のコンベアに4, 8とあったパネルが, 右のコンベアに8, 4の順に運んでいます. パネル2枚だけですが, 上司の要求を満たせています. 

JUMPコマンドを使ってループさせれば, 上司の要求を満たせますので, この問題はクリアーできます.

3)補足

COPYFROMコマンドとCOPYTOコマンドがエラーとなる, つまり, 上司に怒られる条件を確認しておきます.

パネルを持たずにCOPYTOコマンドを実行するとエラーとなり, 上司に怒られます. カーペットにコピーするパネルを持っていないのですから, 当然です. 

パネルを置いていないカーペットに対してCOPYFROMコマンドを実行するとエラーとなり, 上司に怒られます. コピーしようとするパネルが無いのですから, 当然です. 

4)まとめ

  1. この課題では, COPYTOコマンドを使ってカーペットにパネルのコピーを置く方法を学びました. つまり, COPYTOコマンドを使えば持っているパネルを退避できます.
  2. パネルを持たずにCOPYTOコマンドを実行するとエラーとなります. 
  3. パネルを置いていないカーペットに対してCOPYFROMコマンドを実行するとエラーとなります. 

2021/09/24

ヒューマン・リソース・マシーン 入社3年目−コピーのお仕事

 目次

1)課題

左側のコンベアのパネルの内容に関係なく右側のコンベアに「B, U, G」の3文字を運んでください。

庶務の方がいくつかの文字を用意してくれた様です。有難く使わせて頂きましょう。

2)解説

左のコンベアには, 「-99」と書いたパネルが並んでいます. 上司との会話によると, コンベアが壊れてしまったようです. また, 問題文によると, 左のコンベアは放っておいて, カーペットに置いてあるパネルを右のコンベアに運べばクリアーできます. カーペットに置いてあるパネルを取り出すために, この問題からCOPYFROMコマンドを使えるようになりました. 

COPYFROMコマンドを使ってみます. COPYFROMコマンドの右にある数字はカーペットの番号を表します. 図のCOPYFROMコマンドは, 「B」と書いてあるパネルを置いてある4番のカーペットを指しています. 

「ステップ実行」ボタンを2回押し, COPYFROMコマンドを2回実行しました. カーペット4番のパネルをコピーして, 「B」と書いたパネルを持っています. また, カーペットのパネルをコピーしますから, コピー元となるカーペットのパネルは無くならず, そのまま残ります. 

COPYFROMコマンドの使い方が分かったので, 課題のクリアーを目指しましょう. 課題文によると, 「B, U, G」の3文字を運べばクリアーできます. カーペットを見ると, 「B」は4番, 「U」は0番, 「G」は3番に置いてあります. つまり, この順番にCOPYFROMコマンドを並べ, その後ろにOUTBOXコマンドを配置すれば良いのです.

このプログラムを実行すれば, 課題をクリアーできます. 

3)補足

OUTBOXコマンドを取り除いて, ステップ実行をしてみます. 図は, COPYFROMコマンドを3回実行した結果です. 「G」と書いたパネルを持っています. その前に, 「B」と書いたパネルも「U」と書いたパネルを持っていましたが, COPYFROMコマンドを実行すると, 持っているパネルは捨ててしまいます.

4)まとめ

  1. この課題では, COPYFROMコマンドを使ってカーペットに置いてあるパネルをコピーする方法を学びました.
  2. パネルを持ったままCOPYFROMコマンドを実行すると, 持っているパネルを捨ててから, カーペットのパネルをコピーします. 
  3. カーペットのパネルをコピーするだけですから, コピー元となるカーペットのパネルはそのまま残ります. 

2021/09/22

ヒューマン・リソース・マシーン 入社2年目−忙しい運び屋さん

1)課題

 

左側のパネルを順番ずつ、右側に運んでください。

[JUMP]コマンドが使えるようになりました。プログラムの好きな場所にジャンプすることができます。

[JUMP]コマンドを使えば、全部で3行のプログラムで済むハズです。

2)解説

左のコンベアにアルファベットを書いたパネルが沢山並んでいます. 1年目と同じようにINBOXとOUTBOXを並べても良いのですが, もう少し上手い方法を考えてみます.

問題文にもあるように, この問題からJUMPコマンドを使えるようになりました. JUMPコマンドを使うと, ジャンプした先のコマンドから実行しますので, INBOXコマンドとOUTBOXコマンドを繰り返し実行できます. 

JUMPコマンドから矢印が出ていて, INBOXの前にある青い四角形を指しています. このプログラムを実行すると, JUMPコマンドに続けてINBOXコマンドを実行します. このプログラムを実行すれば, 課題をクリアーできます. 

プログラムの実行を開始して, すべてのパネルを運び終わるまでに時間がかかります. 「スライドしてスピードを調整」と吹き出しが出ている緑の四角を左右に動かすと実行速度を変えられます. 実行速度を速めると待ち時間を短縮できます.

勘の良い人なら, 「無限ループになっている. このプログラムは終了しないのでは?」と心配になります.ヒューマン・リソース・マシーンでは, INBOXコマンドを実行した時, 左のコンベアにパネルが無くなっていたら, そこでプログラムは止まります. 

3)まとめ

  1. この課題では, JUMPコマンドを使ってコマンドを繰り返し実行する方法を学びました.
  2. JUMPコマンドを使うことで, いわゆる無限ループの形になりますが, 左のコンベアにパネルが無い時にINBOXコマンドを実行するとプログラムは停止します.
  3. スライダーを使ってプログラムの実行速度を調整できます. 

2021/09/18

ヒューマン・リソース・マシーン 入社1年目−運び屋さん

1)課題

コマンドを組み合わせ、左のコンベアにあるパネルを右のコンベアに運んでください。

[INBOX]は、左コンベアからパネルを1つ手に取ります。

[OUTBOX]は、手元のパネルを右のコンベアに運びます。

2)解説

この課題では, ヒューマン・リソース・マシーンの遊び方を学びます. ヒューマン・リソース・マシーンでは, 左のコンベアからパネルを取り出します. コマンドを組み合わせてパネルを加工し,右のコンベアに運びます. 右のコンベアに運んだパネルが課題を満たせば, クリアーとなります. 

この課題では, パネルを加工せずに, そのまま右のコンベアに運ぶだけです. INBOXコマンドとOUTBOXコマンドの組み合わせを2つ並べて実行してみます. 三角形のボタンに「プログラムが完成したらここを押して実行」と表示してあります. このボタンを押すとプログラムを実行します. ここでは, 実行ボタンの右にある「ステップ実行」ボタンを使います. 

「ステップ実行」を使うと, コマンドを一つ実行してプログラムを停止し, 次に「ステップ実行」ボタンを押すまで待ってくれます. ステップ実行を使うことで, コマンドの動作を詳しく調べられます. 「ステップ実行」使って, INBOXコマンドとOUTBOXコマンドの動作を確認してみましょう.

「ステップ実行」を開始しました. 緑色の三角が次に実行するコマンドを示しています.

INBOXコマンドを実行しました. 左側のコンベアからパネルを1枚取り, 頭上に掲げています.

OUTBOXコマンドを実行しました. 持っていたパネルを右のコンベアに置きました.

再び, INBOXコマンドを実行しました. 2と書いたパネルを取り出しました.

続けて, OUTBOXコマンドを実行しました. 持っていたパネルを右のコンベアに置きました. 

この時点で実行するコマンドが無くなったのですが, 続けてステップ実行をしてみました. 実行するコマンドが無くなったので, プログラムは停止します. プログラムが停止した時に, 右のコンベアのパネルが課題を満たしていないので上司が怒っています. 

上司の要求に応えるためには, INBOXとOUTBOXの組み合わせがもう一つ必要です. このプログラムを実行すると, 課題をクリアーできます. 

3)補足:INBOXとOUTBOXの細々としたこと

ここまでできれば, 次の課題に進んでも良いのですが, もう少しINBOXとOUTBOXを調べてみます.

プログラムを開始する前, 左のコンベアに4, 7, 8と書かれたパネルが乗っています. INBOXコマンドを連続して実行したらどうなるかを調べてみます.

INBOXコマンドを続けて2回, 実行してみました. 7と書いたパネルを持っていますが, 4と書いたパネルが見当たりません. 1回目のINBOXコマンドを実行すると4と書いたパネルを持ちますが, 2回目のINBOXを実行すると, 左のコンベアからパネルをとるために, その時に持っていたパネルを捨ててしまいます. 

同じようにして, OUTBOXコマンドを続けて2回, 実行してみました. OUTBOXコマンドを実行すると, 持っているパネルを右のコンベアに置きます. 持っていたパネルをコンベアに置いたので, OUTBOX実行後には何もパネルを持っていません. パネルを持たずにOUTBOXコマンドを実行すると, 『「空のデータ」だよ!』と上司に怒られてしまいます. 

4)まとめ

  1. この課題では, ヒューマン・リソース・マシーンの遊び方を学びました.
  2. INBOXコマンドとOUTBOXコマンドの働きを学びました.
  3. ステップ実行を使うことでプログラムの動きをコマンドごとに確認できます.
  4. INBOXコマンドを実行する時, パネルを持っていれば, そのパネルは捨ててしまいます.
  5. パネルを持たずにOUTBOXコマンドを実行すると上司に怒られます.

2021/09/15

子どもだけでなく大人も楽しめるプログラミング学習法

電車やバスに乗ると多くの人がスマートフォンに見入っている光景を目にします. スマートフォンを通してインターネットにつながり, 情報を得るだけでなく発信もできようになり, 社会の在り方も大きく変わりました. 例えば, 通勤の途中で買い忘れていた日用品をAmazonなどのECサイトで注文する, といったことが当たり前になりました.

社会の変化に合わせて, 企業や官公庁もインターネットを中心とした業務の進め方に変えていくためDX(デジタルトランスフォーメーション)に取り組んでいます. DXを推進するためには, コンピュータ, ネットワーク, プログラミングに関する知識を持った人材, いわゆるIT人材が必要です.

しかし, IT人材は不足しており, 経済産業省は2030年には約80万人のIT人材が不足すると報告しています. IT人材不足に対応するため, 文部科学省は2020年度から小学校でのプログラミング教育を必須化し, 2025年1月に実施する大学入学共通テストからプログラミングなどを出題する「情報」が導入さる予定です. 10年後にはプログラミングなどできて当たり前, といった社会を目指しているのでしょう.

では, 学校教育でプログラミングに関する授業をすることで, プログラムを作れるようになるでしょうか? 中学・高校で英語の授業を受けましたが, 学校の授業だけで英語はバッチリ, という人はどれぐらいいるでしょうか? 英語の教師は, 英語の素養があり, 教えるために特別な訓練を受けていますが, それでもこの有様です. プログラミングを担当する教師はどうでしょう? 

プログラミングに関して学校教育は当てにならないと, 多くの親御さんは直観的に分かっているので, 大手の学習塾ではプログラミング教室を開催しています. プログラミング教室に通わせるためには費用も掛かりますので, 費用に見合う成果を期待するでしょう. しかし, プログラミングに関する知識の無い親御さんが, どうすれば成果が得られたと判断できるのでしょうか? 

プログラミング教室に通えば, 子どもが作ったというプログラムを見せてもらえるかもしれません. 一から十まで, 何も無いところから作り上げたのなら立派なものですが, プログラミング教室も時間が限られていますので, プログラムの全体的な枠組みを与え, 子どもは少し修正しただけかもしれません. 

小学校でプログラミングを必須化した目的は, 論理的思考能力を身に付けることにあります. 論理的思考能力が身に付いたと判断できれば, プログラミング教室に通わせた価値があったと言えます. 論理的思考能力が身に付いたと判断するには, プログラミングが・・・, 元に戻ってしまいました. 

つまり, プログラミングの経験の無い人でも, プログラムを作れている, 論理的思考能力が身に付いている, と判断できる方法が必要です. 判断方法の一つとして, ヒューマン・リソース・マシーンというプログラミング・パズルゲームが使えそうです.

ヒューマン・リソース・マシーンでは, ゲーム内で上司から与えられた課題を解決するためにコマンドを並べ, 主人公を操作します. この「コマンドを並べ」がプログラミングに相当します. 

ヒューマン・リソース・マシーンで出される課題は, 「掛け算のしかた」や「素因数に分解せよ」などの簡潔なものばかりで, 「できた・できていない」を容易に判断できます. しかも, 使用できるコマンドの種類が少ないので簡単に覚えられます. このゲームでは, 論理的に考えることが求められます.

ヒューマン・リソース・マシーンは, SwitchやiOSなど様々なプラットフォームでリリースされています. また, 価格もSwitch版は1000円, iOS版は610円と安価であり, 手軽に始められます.

しかし, プログラミング経験の無い人が, ヒューマン・リソース・マシーンで出題される課題を解くためのアルゴリズムを考え出すのは大変だと思います. そこで, 課題を解決するための考え方を中心に説明していきます.

2021/09/11

TypeScript 第7回 ファイル読み込み

第5回で, コマンドライン引数から式を与えられるようにしました. 次は, ファイルからの読み込みに対応したくなります. この記事では, ファイルを読み込むために調べた結果を書きます. 

1)fs.readFileSync関数

Node.jsのfsモジュールに定義してある関数を使えば, ファイルを読み込めます. package.jsonファイルを読み込んで, コンソールに出力するコードは次のようになります.

import * as fs from 'fs'
let text = fs.readFileSync('./package.json')
console.log(text.toString())

1行目でfsモジュールをインポートし, 2行目でreadFileSync関数を使ってpackage.jsonファイルを読み込み, 結果をtextに設定しています. 3行目でコンソールに出力していますが, toString()を使って文字列に変換しています. 

この結果を使えば, ファイルから逆ポーランド記法で記述した式を読み込み, 計算できます. 

2)非同期処理

readFileSync関数を使えば, 目的を達成できますが, TypeScriptを使いこなすためには, fsモジュールのreadFile関数を避けて通れません. readFile関数の使い方をもう少し調べます.

readFile関数には, ファイルから読み込んだ結果を処理するためのコールバック関数を与えます. コードを書くと次のようになりました. 

import * as fs from 'fs'

fs.readFile('./package.json', (errorresult=> {
    if (error) {
        console.log('エラー'error)
    } else {
        console.log(result.toString())
    }
})

console.log('プログラム終了')

実行した結果は次のようになりました.

PS C:\TSWork\ReadFile> .\node_modules\.bin\tsc
PS C:\TSWork\ReadFile> node .\dist\index.js
プログラム終了
{
  "name": "readfile",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^16.6.1",
    "typescript": "^4.3.5"
  }
}

PS C:\TSWork\ReadFile>

ファイルの読み込みを非同期に実行するため, プログラムの最後に書いてある「プログラム終了」をコンソールに出力する処理が終わってから, ファイルを読み込んだ結果を表示しています. また, readFile関数の戻り値はvoidであるため, readFileSync関数のようにファイルから読み込んだ結果を返しません. このままでは, コールバック関数の中に処理を書き始めてしまい, 所謂, コールバック地獄に陥ってしまいます. 

3)Promise

コールバック地獄から逃れるためにPromiseを使います. コードは次のようになりました. 

import * as fs from 'fs'

function readTextFile(filepathstring): Promise<string> {
    return new Promise((resolvereject=> {
        fs.readFile(filepath, (errorresult=> {
            if (error) {
                reject(error)
            } else {
                resolve(result.toString())
            }
        })
    })
}

async function readExec() {
    console.log('ファイル読み込み開始')
    let text = await readTextFile('./package.json')
    console.log(text)
}

readExec()

console.log('プログラム終了')

readTextFile関数では, fs.readFile関数の実行をPromiseで包んで返すようにしています. 

readTextFile関数は非同期に実行するので, 実行結果を待つためにawaitを指定します. awaitを使うためには, asyncで囲まなくてはなりませんので, readExec関数を定義しています. 実行結果は次のようになりました.

PS C:\TSWork\ReadFile> .\node_modules\.bin\tsc
PS C:\TSWork\ReadFile> node .\dist\index.js
ファイル読み込み開始
プログラム終了
{
  "name": "readfile",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@types/node": "^16.6.1",
    "typescript": "^4.3.5"
  }
}

PS C:\TSWork\ReadFile>

相変わらず, 「プログラム終了」をコンソールに出力してから, ファイルから読み込んだ結果を出力していますが, readTextFile関数の結果を変数に代入できるので, 扱いやすくなりました. 

4)まとめ

この記事では, ファイルから読み込む方法を調べました. ファイルの読み込みは非同期で実行するため, 慣れるまで時間がかかりそうです. 最後に, この記事で使用したNode.jsとTypeScriptのバージョンは次の通りです.

PS C:\TSWork\ReadFile> node --version
v14.17.4
PS C:\TSWork\ReadFile> .\node_modules\.bin\tsc --version
Version 4.3.5
PS C:\TSWork\ReadFile>

2021/09/08

TypeScript入門 第6回 モジュール定義

これまで, ソースコードはindex.tsファイルに書いてきました. プログラムもまとまってきましたので, ソースファイルを管理しやすくするために, ファイルを分割してモジュールを定義します.

1)ソースファイルを分割する

srcディレクトリにrpn.tsファイルを作り, index.tsファイルのほぼすべての内容, データ型の定義と関数定義をコピーします. index.tsファイルに関数定義を残したままにしておくと, コンパイラがエラーを報告します.


rpn.tsファイルのparse関数とcalc関数にexportを付けるとエラーが解消します


index.tsから関数定義を削除すると, 関数が見つからないとコンパイラはエラーを報告します.

この結果から分かったことは次の通りです.

  1. 新しく作ったrpn.tsファイルにindex.tsから関数をコピーしただけでは, rpn.tsファイルの内容はindex.tsと同じモジュールにあるものとして扱う.
  2. rpn.tsファイルで公開する関数にexportを付けるとrpn.tsファイルの内容を別のモジュールとして扱う.

TypeScriptの仕様を確認したわけではないので, 間違えていたらスミマセン.

2)モジュールから関数をインポートする

関数をインポートしてみます. 

import {parsecalcfrom './rpn'

let input = process.argv.slice(2process.argv.length)
// 計算してみる
console.log('解析結果'parse(input))
console.log('計算結果'calc(parse(input)))

1行目のimport文を追加すると, コンパイルエラーは解消しました. この方法であれば, index.tsを修正せずに済みますが, rpn.tsファイルで定義してある関数であることを明示する方が私の好みです.

import * as rpn from './rpn'

let input = process.argv.slice(2process.argv.length)
// 計算してみる
console.log('解析結果'rpn.parse(input))
console.log('計算結果'rpn.calc(rpn.parse(input)))

3)まとめ

この記事では, ソースファイルを分割してモジュールを定義する方法を調べました. ただし, この記事の内容はコンパイラの応答から推測した結果をまとめただけであり, 言語仕様などを確認する必要があります. また, 実行環境等によって違いがあるようなので注意が必要です. 

最後に, この記事で使用したNode.jsとTypeScriptのバージョンは次の通りです.

PS C:\TSWork\Module> node --version
v14.17.5
PS C:\TSWork\Module> .\node_modules\.bin\tsc --version
Version 4.4.2
PS C:\TSWork\Module>

2021/09/04

TypeScript入門 第5回 データ型定義

前回, コマンドライン引数から逆ポーランド記法で記述した式を入力できるようにしました. 入力のチェックを何もしていないので, 対策していきます. まずは状況の確認から始めます.

1)エラーが生じる条件

プログラムには想定している入力の範囲があります. 作成しているプログラムでは, 次のような入力を想定しています.

  1. 数値
  2. 四則演算子(+, -, ×, /)

想定していない誤った式を入力してみます.

PS C:\TSWork\RPN> node .\dist\index.js 9 A + 4 *
計算結果 NaN

9にAを加えていますが, AをNaNと解釈したため, 計算結果もNaNとなっています. もう一つ試してみます.

PS C:\TSWork\RPN> node .\dist\index.js 9 3 * 4 %
C:\TSWork\RPN\dist\index.js:39
        throw new Error('スタックの要素数が1ではありません. 式を間違えているかも?');
        ^

Error: スタックの要素数が1ではありません. 式を間違えているかも?
    at calc (C:\TSWork\RPN\dist\index.js:39:15)
    at Object.<anonymous> (C:\TSWork\RPN\dist\index.js:44:21)
[90m    at Module._compile (internal/modules/cjs/loader.js:1072:14)[39m
[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)[39m
[90m    at Module.load (internal/modules/cjs/loader.js:937:32)[39m
[90m    at Function.Module._load (internal/modules/cjs/loader.js:778:12)[39m
[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)[39m
[90m    at internal/main/run_main_module.js:17:47[39m
PS C:\TSWork\RPN>

余りを求める演算子のつもりで「%」を使っています. 「%」もNaNと解釈しているため, このようなエラーを表示しています. 最低限の対策として, 文字列をNaNと解釈した場合にエラーを投げるようにすれば良さそうです.

2)エラー処理の方針

このまま対策を入れてしまうと, 逆ポーランド記法で記述した式を計算するcalc関数が複雑になってきます. そこで, エラー処理を2つに分けます.

  1. 入力した文字列が想定している範囲内にあることを確認する。
  2. 式の構造が正しいことを確認する。

式の構造の正しさは, calc関数で計算を進めながら確認していきます. この確認は, 既にcalc関数に作ってあります. 足りないことは, 入力文字列が想定内であることを確認する処理です. これを作っていきます.

3)データ型の定義

入力文字列をチェックする関数など簡単に作成できますが, もう少し考えを進めます. この程度のプログラムではありえないのですが, 2人のプログラマが関わっており, 入力をチェックする部分と, 計算する部分に担当が分かれているとします. 2人の間でうまく連携を取らないと, 入力のチェック後に計算担当へ文字列配列を渡した後で, 計算の担当者が受け取った文字列配列をチェックし始めます. 

二重にチェックをしてしまうと, 仕様変更時の修正箇所が増えてしまい, 修正漏れがあると不具合の原因となります. そこで, 2人のプログラマの間での取り決めとして, データ型の配列を渡すようにします. データ型を定義して, プログラムを修正した結果は次の通りです.

// 数式を構成する要素に対応するデータ型の定義
type Token = 
        | 'ADD'   // 足し算
        | 'SUB'   // 引き算
        | 'MUL'   // 掛け算
        | 'DIV'   // 割り算
        | number  // 数値

// 文字列からTokenを作る
function makeToken(strstring) {
    let tokenToken
    switch (str) {
        case '+'token = 'ADD'break
        case '-'token = 'SUB'break
        case '*'token = 'MUL'break
        case '/'token = 'DIV'break
        default:
            let n = parseFloat(str)
            if (isNaN(n)) {
                throw new Error(`対応していない入力です:${str}`)
            } else {
                token = n 
            }
    }
    return token
}

// 文字列の配列をTokenの配列に変換する.
function parse(inputArraystring[]) {
    let resultToken[] = []
    for (let value of inputArray) {
        result.push(makeToken(value))
    }
    return result
}

// 補助関数
function calcAux(stacknumber[], op: (anumberbnumber=> number) {
    let b = stack.pop()
    let a = stack.pop()
    if ('number' === typeof a && 'number' === typeof b) {
        stack.push(op(ab))
    } else {
        throw new Error('数字が入ってないよ')
    }
}

// 配列を逆ポーランド記法で記述した式として計算する.
function calc(inputToken[]) {
    let stacknumber[] = []
    for (let token of input) {
        switch (token) {
            case 'ADD' : calcAux(stack, (ab=> a + b); break
            case 'SUB' : calcAux(stack, (ab=> a - b); break
            case 'MUL' : calcAux(stack, (ab=> a * b); break
            case 'DIV' : calcAux(stack, (ab=> a / b); break
            default : stack.push(token)
        }
    }

    if (1 === stack.length) {
        return stack.pop()
    } else {
        throw new Error('スタックの要素数が1ではありません. 式を間違えているかも?')
    }
}

let input = process.argv.slice(2process.argv.length)
// 計算してみる
console.log('解析結果'parse(input))
console.log('計算結果'calc(parse(input)))

修正した内容を説明します. まず, データ型の定義から.

// 数式を構成する要素に対応するデータ型の定義
type Token = 
        | 'ADD'   // 足し算
        | 'SUB'   // 引き算
        | 'MUL'   // 掛け算
        | 'DIV'   // 割り算
        | number  // 数値

式を構成する演算子や数値を表すデータ型を定義します. 演算子に対応した文字列リテラルとnumberの合併としています. これだけ見ると, わざわざ型を導入する必要があるのか, と自分でも疑問を感じますが, 後でありがたみが分かってきます.

// 文字列からTokenを作る
function makeToken(strstring) {
    let tokenToken
    switch (str) {
        case '+'token = 'ADD'break
        case '-'token = 'SUB'break
        case '*'token = 'MUL'break
        case '/'token = 'DIV'break
        default:
            let n = parseFloat(str)
            if (isNaN(n)) {
                throw new Error(`対応していない入力です:${str}`)
            } else {
                token = n 
            }
    }
    return token
}

文字列をToken型に変換するための関数を定義しました. 文字列の配列のままならこの関数も不要なので, Token型を作らなければ良かったのでは, と後悔し始めています.

// 文字列の配列をTokenの配列に変換する.
function parse(inputArraystring[]) {
    let resultToken[] = []
    for (let value of inputArray) {
        result.push(makeToken(value))
    }
    return result
}

文字列の配列をTokenの配列に変換するための関数も定義しました. Token型を導入したばかりに, コード量が2倍に膨れ上がりました. 失敗か?

// 配列を逆ポーランド記法で記述した式として計算する.
function calc(inputToken[]) {
    let stacknumber[] = []
    for (let token of input) {
        switch (token) {
            case 'ADD' : calcAux(stack, (ab=> a + b); break
            case 'SUB' : calcAux(stack, (ab=> a - b); break
            case 'MUL' : calcAux(stack, (ab=> a * b); break
            case 'DIV' : calcAux(stack, (ab=> a / b); break
            default : stack.push(token)
        }
    }

    if (1 === stack.length) {
        return stack.pop()
    } else {
        throw new Error('スタックの要素数が1ではありません. 式を間違えているかも?')
    }
}

逆ポーランド記法で記述した式を計算するための関数本体です. 引数inputの型を文字列の配列からTokenの配列に変更しています. Token型に変更したので, ifからswitchに変更しました. スッキリとしました. 

let input = process.argv.slice(2process.argv.length)
// 計算してみる
console.log('解析結果'parse(input))
console.log('計算結果'calc(parse(input)))

最後に, コマンドライン引数をcalc関数に渡すため, parse関数を間に挟んでいます. コンパイルして実行した結果を確認します. 

PS C:\TSWork\RPN> .\node_modules\.bin\tsc
PS C:\TSWork\RPN> node .\dist\index.js 9 4 * 6 /
解析結果 [ 9, 4, 'MUL', 6, 'DIV' ]
計算結果 6
PS C:\TSWork\RPN> node .\dist\index.js 9 4 * 6 %
C:\TSWork\RPN\dist\index.js:21
                throw new Error("\u5BFE\u5FDC\u3057\u3066\u3044\u306A\u3044\u5165\u529B\u3067\u3059:" + str);
                ^
Error: 対応していない入力です:%
    at makeToken (C:\TSWork\RPN\dist\index.js:21:23)
    at parse (C:\TSWork\RPN\dist\index.js:34:21)
    at Object.<anonymous> (C:\TSWork\RPN\dist\index.js:79:21)
[90m    at Module._compile (internal/modules/cjs/loader.js:1072:14)[39m
[90m    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1101:10)[39m
[90m    at Module.load (internal/modules/cjs/loader.js:937:32)[39m
[90m    at Function.Module._load (internal/modules/cjs/loader.js:778:12)[39m
[90m    at Function.executeUserEntryPoint [as runMain] (internal/modules/run_main.js:76:12)[39m
[90m    at internal/main/run_main_module.js:17:47[39m
PS C:\TSWork\RPN>
エラーをキャッチしていないのはどうかと思いますが, 入力に応じた計算ができています.

4)型システムの恩恵

Token型を導入したありがたみを確認しておきます. calc関数では, switch文を使って演算子の種別に応じた計算をしています. 対応していない演算子がある場合, コンパイラが怒ってきます.


Token型を導入したことにより, 演算子を追加した場合の対応漏れを防ぐことができます. このようにすれば, つまらない不具合をコンパイル時に潰せるようになります. ただし, 型システムの恩恵に与るには, 型システムにチェックしてもらえるようにデータ型を定義する必要があり, それなりに経験が必要になります.

5)まとめ

この記事では, コマンドラインから入力した式をチェックするためにデータ型を導入しました. データ型を導入することで, プログラミング時にありがちな誤りをコンパイラにチェックさせる方法を確認しました. 

最後に, この記事で使用したNode.jsとTypeScriptのバージョンは次の通りです.

PS C:\TSWork\RPN> node --version
v14.17.4
PS C:\TSWork\RPN> .\node_modules\.bin\tsc --version
Version 4.3.5
PS C:\TSWork\RPN>

ヒューマン・リソース・マシーン 入社41年目−並べ替えよ

目次 1)課題 0を終端とした文字列がいくつか流れてきます。各文字列に対してソート(並べ替え)を行い、小さい順(昇順)に右側へ運んでください。 2)状況の確認 この問題では, 予めコードが入っています. このコードを実行して, 何をするコードなのか確かめます.  左のコンベアから...