Lua入門 その3 — 数値と計算

目次

Unknown programmer's programming note.

<2022-01-23 日>

1 何をするか

今回はもう少し実用的な計算をやってみます。 単に計算するだけではなく、コマンドライン引数として値を受け取る方法も扱います。

2 問題3

今回メインの問題は次の通りです。

問題3 — BMIの計算

コマンドライン引数で身長(cm)と体重(kg)を受け取り、BMIを表示する。

BMIの計算方法は次の通り:

\(\text{BMI} = \text{体重(kg)} \div \text{身長(m)}^2\)

2.1 BMIとは

BMIとは Body Mass Index の略です。 日本語にするとボディマス(体重あるいは肥満)指数です。 身長と体重から肥満度を数値で表すためのものです。

もし、身長が200cmの人と160cmの人がいたとして、二人とも体重90kgだとします。 この場合、200cmの人は太ってるとは言えないですが、160cmの人はそうでもないでしょう。

BMIの値によって、痩せてるのか普通なのか太ってるのかは、基準値があります。 ダイエットをしている人には目安になる数値です。

2.2 問題の吟味

計算自体は簡単だと思います。 しかし、いくつか考えなければいけないことがあります。

  • コマンドラインで引数を受け取る方法。 今回は絶対必要になる。
  • 文字列を数値に変換する方法。 コマンドライン引数から受け取った引数は文字列なので、数値に変換する必要ある。
  • 単位の変換。 問題では、コマンドラインでは身長をセンチメートルで受け取るが、BMIの計算にはメートルを用いなければいけない。
  • 一時的な計算結果に名前を付けて保存しておく方法。 必須ではないけど、できた方が遥かに便利。
  • コマンドライン引数のエラーを扱う方法。 例えば引数が一つしか指定されなかった場合など。

この中で特に重要なのは、一時的な計算結果に名前を付けて保存しておく方法で、変数と呼ばれるものを利用する必要があります。 変数は極めて重要なので、独立したページを用意したいくらいです。 この問題においては、変数を利用せずとも要求を満すことが可能です。 しかし、利用しないとかなり冗長で分かりにくいコードになってしまうため、ここでさらっとだけ触れて、実際に使うことにします。

エラーについては完全に無視します。 エラーによって処理を振り分けるためには、条件によって分岐するための機能が必要になります。 あまり詰め込み過ぎたくないのでここではやりません。 エラーが発生したら、エラーメッセージがそのまま表示されることとなるのですが、無視します。

コマンドライン引数を扱うのは、Luaでは比較的簡単です。 簡単というのは、今回のような単純なケースの場合では簡単ということです。 特にエラーを無視することにしたので、すごく簡単です。 ユーザーが指定したコマンドライン引数は、argという特別に名前のついた変数に格納されます。 その変数はテーブルであり、配列でもあります。 テーブルも配列も大きなテーマであり、ここでもまた軽く触れる程度にしておきます。

文字列を数値に変換するのは、エラーを無視するので、やはり簡単です。 あらかじめ用意されたLuaの関数、tonumberというのを利用するだけです。 関数の使い方は、printがそうであったように、tonumber(関数の引数)とします。 しかし、tonumber関数は、処理を行った結果を戻り値として返してきます。 それを利用する必要があります。 print関数のように、画面に表示されるという副作用を目的とするものではありません。

単位の変換は簡単です。 1メートルは100センチメートルなので、センチメートルからメートルに変換するには、100で割れば良いです。 なぜコマンドライン引数をセンチメートルで取るようにしたかというと、日本では日常的には身長にセンチメートルを使うからです。 その方がプログラムの使い勝手がいいと思ったからです。

ちょっと長くなってしまいましたが、考慮すべき点はこのくらいです。

2.3 作業場所を確保する

これまでと同じように、問題のための専用のディレクトリを用意します。

% cd ~/code/pintro⏎ ▹このシリーズ用のディレクトリに移動 ~/code/pintro は前回までで作成済とする
% mkdir problem3⏎   ▹この問題用のディレクトリを作成
% cd problem3⏎      ▹作成したディレクトリの中に移動
% pwd⏎              ▹カレントディレクトリのパスを表示
/home/toma/code/pintro/problem3

ユーザー名 toma のところは、自分のユーザー名に置き換えて下さい。

作業場所はこれで用意できました。

2.4 コードを書く

コードを書くといっても今の状態では手が付けられません。 覚えることがいくつかあります。

  • 前回の単純な計算よりも、もう少し複雑な計算のやり方。
  • 一時的な計算の結果や値に名前を付けておく方法(変数)。
  • コマンドライン引数を受け取る方法。
  • 文字列を数値に変換する方法。

主にこの4つです。

2.4.1 計算を組合せる実験

まず完成形を目指す前に、計算の実験をやっておきます。 前回でやった計算は、足し算、引き算、掛け算、割り算の4つでした。 それは 1 + 1 とか 2 - 3 とか 3 * 5 とか 9 / 3 とかでした。 どれも、計算が1回だけしか登場しない場合しか試していません。 ここで、この計算のことを演算と呼ぶことにします。 そして、+とか-とか*とか/とかの演算のための記号を演算子と呼ぶことにします。

前回までは、一度の計算で演算子が一つしかない、つまり演算が1回の場合しかやっていなかったことになります。 Luaでは、一度の計算に複数個の演算子を含め、複数回の演算を行なわせることが可能です。 そして、それはごく普通に、頻繁に利用します。

例えば、次のような計算です。

1 + 2 + 3 + 4     → 10
1 + 2 - 3 * 4     → 1 + 2 - 12 → -9
1 * 2 + 3 / 4     → 2 + 0.75 → 2.75
(1 + 2) / (3 - 4) → 3 / -1 → -3.0

対話モードで試す場合、右矢印は、どのようなステップで計算がされるかを示しただけのものですので、入力不要です。

この少し複雑な計算式には、ちょっとしたルールがあります。

  • 基本的には、左から右に向って計算される。
    • ただし、掛け算「*」と割り算「/」は、足し算「+」引き算「-」より先に計算される。
      • ただし、括弧「(」と「)」に囲まれた部分は、先に計算される。

要は、おそらく実世界で採用されている、初歩的な計算のルールと大体同じということです。

実際にLuaの対話モードで、手に馴染むまでいろいろ入力してみると良いです。

そんなにびっくりすることは起きないはずです。 と言いたいところですが、話はそう単純ではないです。 10とか100とかその辺りの数だけ触っているうちは大丈夫だと思います。 しかし、極端に小さな数や大きな数や浮動小数点数を触っていると、実世界とは違うところがあることに気づくかもしれません。 ですが、ここでは深入りしません。

これで、前回の4つの演算子を組合せて、自由に計算ができるようになりました。 自由にとは言い過ぎかもしれません。 まだ慣れてなくても全然OKです。 とりあえずは、先の例の 1 + 2 - 3 * 4 ぐらいのが理解できれば十分この問題は解決できます。

2.4.2 BMIの計算式の最終的な形

計算ができることは分かりました。 これで最終的にどのような形のコードになるのかが推測できます。 もし、身長が200cmで、体重が100kgならば、次のような計算式になるはずです。

100 / ((200 / 100) * (200/ 100))

200 / 100 としているのは、センチメートルからメートルへ変換する必要があるからです。 さらに、それを掛け算しているのは、二乗する必要があるからです。

この計算を1ステップ進めると、次のようになります。

100 / (2.0 * 2.0)

さらに1ステップ進めます。

100 / 4.0

最後のステップを進めると、BMIの値が得られます。

25.0

もし、身長が200cmで体重が100kgと固定で良いなら、この値を表示すればいい訳です。 それには、最初の計算式をprint関数に渡せば良いです。

print(100 / ((200 / 100) * (200 / 100)))

実際にコードを書いて実行してみれば、結果は 25.0 と表示されるのを確認できるでしょう。

計算式の大体の構造は分かりました。 あとはこれを改造していけば良いです。

一度問題に戻ると、やらなければいけない計算は \(\small \text{BMI} = \text{体重(kg)} \div \text{身長(m)}^2\) というものでした。 ここで、体重と身長という名前に着目します。 もし 体重を weight として、メートルでの身長 height とすることができたらどうでしょう。 さっきのLuaのコードは次のようになります。

print(weight / (height * height))

感覚によるところもあるかもしれませんが、ずっと読み易くなったのではないかと思います。 計算式の完成形としては、これを目指します。

今はまだ weight と height にどのような値が入っているのか分からないため、このままでは何もできません。 次はそれをやります。

2.4.3 数値に名前を付ける

さっきのチャンクは動きません。 それは weight と height とコード上にいきなり出てきても、それが何なのか、Luaには全く分からないからです。 いくらプログラマーが「weightは100で、heightは2.0だ」と念じてみたところで、コンピューターには伝わりません。

脳とコンピューターを繋ぐ技術で将来可能になるかもしれないことを否定はしません。

解決策はストレートで、コード上にwegihtとheightという名前を用意してやることです。 そして、その名前にはそれぞれ体重とメートルでの身長の数値を結び付けてやれば良いです。 そのためにLuaでは次のように書くことができます。

名前 = 値

これは、全く正確ではないので、大体こん感じ?程度に止めておいてください。

「=」がポイントです。 「=」の左側に、右側の値が代入されます。 なので代入文と呼びます。 そして、代入先である左側の名前は、コード上それ以降のどこからでも使えるようになります。 この値に結び付けられた名前は、変数と呼ばれます。

「結び付ける」という表現に違和感を感じられるかもしれません。 実際には、変数はメモリ上のどこかに値を保持していることになります。 つまり、変数とは値を保持しているどこかの場所である、という方が正確です。

以上を踏まえた上で、今やるべきことは、weightとheightという名前の変数を用意してやることです。 まずweightは、次のように書くことができます。

weight = 100

次にheightは、次のように書くことができます。

height = 200 / 100

「=」の右側は、単なる値だけでなく、計算式とすることもできるのは大変重要です。 これらをまとめると、体重100kg、身長200cmの人のBMIを表示するプログラムは次のようになります。

weight = 100
height = 200 / 100
print(weight / (height * height))

一度実行してみます。 このチャンクを含めたファイルを bmi.lua とでもしておきます。 コマンドラインで次のようにして実行します。

% lua54 bmi.lua⏎
25.0

このようになればOKです。

これで大体半分くらいです。

2.4.4 コマンドライン引数を受け取る

コマンドライン引数にアクセスするには主に2つ方法があります。

  • グローバル変数arg
  • ... (3つのドット)

ここではグローバル変数argの方を扱います。

前の節で、変数について軽く知りました。 グローバル変数argも、先のweightやheightのような変数であることには違いありません。 ただ、Luaが自動的に用意してくれるもの、というところに違いがあります。 また、グローバル変数argは、テーブルです。 テーブルについては、今回触れません。 ただ、どうすれば目的のコマンドライン引数にアクセスできるかだけを示します。

問題では、コマンドライン引数として 身長 体重 の順に受け取るのでした。 つまり、一つ目が身長で、二つ目が体重ということになります。 チャンクを含めたファイルを bmi.lua とすると、身長と体重を指定して、このチャンクを起動するためのコマンドは次のようになります。

% lua54 bmi.lua 200 100⏎

このとき、身長として渡された値200には、次のようにしてアクセスできます。

height_cm = arg[1]

体重として渡された値100には、次のようにしてアクセスできます。

weight = arg[2]

arg[1]arg[2] の角括弧「[」と「]」がポイントです。 間には、引数の順番に合わせて、数字を入れます。 例えば、1番目の引数なら[1]、2番目の引数なら[2]、3番目の引数なら[3]といった具合です。

忘れてはいけないのは、argから取得した値は文字列であるということです。 例えコマンドラインで200や100のように数値の書式で指定したとしてもです。 このことは次の節でもう少し詳しく扱います。

一度次のようなコードを書いて実験しておきます。

print(arg[0], arg[1], arg[2], arg[3])

このチャンクを含めたファイルを args.lua とでもしておきます。 そして、次のコマンドでチャンクを実行します。

% lua54 args.lua 200 100⏎
args.lua	200	100	nil

arg[0]に対応するのは、実行したチャンクのファイル名です。 それに続いて、arg[1]、arg[2]、arg[3]の値が表示されています。 注目すべきは3つ目のnilという謎の値です。 これは、何ものでもない、無である、というような意味の値です。 3つ目の引数を指定しなかったため、こうなりました。 値が設定されていない変数にアクセスしようしてnilに遭遇することは今後も経験するでしょう。

もし、コマンドライン引数を一つも指定しなかったら、arg[1]とarg[2]もnilになります。 そして、nilであるかどうかをチェックすることで、エラーの処理を行うことができます。 しかし、今回はエラーを一切無視することにしたので、今回はこれ以上触れないでおきます。

ともかく、一つ目の引数と二つ目の引数にアクセスする方法は分かりました。

2.4.5 文字列を数値に変換する

先にも延べた通り、コマンドライン引数を保持しているグローバル変数argから取得できる値は文字列です。 仮にコマンドライン引数が200と100だったとします。 このとき、arg[1]は "200" で、arg[2]は "100" です。 このarg[1]とarg[2]に計算を適用したらどうなるでしょう。 例えば、次のようにしてみます。

height = arg[1] / 100

次のどちらかの可能性が考えられます。

  • 文字列 / 100 のような計算はできない。
  • 文字列"200"は、200という数値として解釈できるのだから 200 / 100 と同じ結果になる。

Luaでは後者の方になります。

対話モードで次のような計算式を試してみると良いです。

> "10" + 1
11
> 10 + "1"
11
> "10.1" + 1
11.1

このような演算子によって強制的に文字列を数値に変換したり、また、その逆で数値から文字列に変換したりすることを、Luaではcoercion(強制)と呼びます。 coercionが働くので、特に何もしなくてもグローバル変数argから取得した文字列の値を、そのまま計算に使えます。

以上から、最終的なコードは次のようになります。

height = arg[1] / 100
weight = arg[2]
print(weight / (height * height))

一応、coercionに頼らず、積極的に文字列から数値に変換する方法も紹介しておきます。 その目的のためのtonumberという関数が用意されています。 tonumberを使えば、最終的なコードは次のようになります。

height = tonumber(arg[1]) / 100
weight = tonumber(arg[2])
print(weight / (height * height))

他の多くの言語では、coercionのような仕組みはないので、tonumberを使った方が他言語のプログラマーには親切かもしれません。

最初のバージョンの完成形として、どちらを採用するかですが、tonumberを使わない方にします。 これだけ短いプログラムであれば、後で見直したときに「何で文字列に対して計算をしてるんだ?」と疑念を抱くこともそんなにないはずだと思うからです。 むしろ、Luaの特徴を示していている良いサンプルになるかもしれません。

以上を持って問題の解答とします。

2.5 動かしてみる

作っただけではだめで、適当に数字を入力して期待通りの結果が表示されるか確認しておきます。 また、数値として解釈できない、間違ったコマンドライン引数を与えて、どのような動作になるのかも確認しておきます。 さらに、引数の数が足りない場合と引数の数が多すぎる場合も確認しておきます。

ここではどうなるのかを示すのは止めておきます。

3 まとめ

このページではBMIの計算する問題を通して、以下のことを扱いました。

  • 複数の演算を組合せた計算
  • 変数
  • コマンドライン引数を受け取る方法
  • 文字列を数値に変換する方法

一応、BMIが知りたいというときに、まあ使っても良いかなという程度のものにはなったかもしれません。 しかし、致命的な欠陥を抱えています。 それはエラーを完全に無視していることです。 そのため、コマンドライン引数が間違っていると、画面に意味不明なエラーメッセージが表示されることになります。 ユーザーにとって大変不親切です。 実用的なプログラムとしては不合格です。

今後、もう少しLuaの機能を取り上げたら、もう一度この欠陥を修正することにします。

著者: watercat

Created: 2023-03-19 日 01:34

Emacs 27.1 (Org mode 9.3)

Validate