知人が趣味で C 言語の勉強を始めたそうだ。
Pascal は大学の時に学んだらしいが、その程度の知識。趣味でもプログラムは作っていない。
C の方が「よりハードウェアを理解できる」と聞いてやってみようと思った、とのことで、実用性を求めていない。なので、C++ でも、Python や Javascript でもない。
まぁ、そこは良い。「わかんないところがあるんだけど」と質問されて、それが面白かったので書き留めておきたい、と思っただけだ。
彼がわからなかったのは、プログラム自体の質問ではない。
初心者向けでもないが、上級者なら当たり前すぎて疑問にも思わない。中級者くらいが気になる質問だった。
本やネットで答えを探しても、こうした中級者向けの話題自体が少ないそうだ。
▼平均を求めよ、という例題があったのだけど、足してから割るのと、割ってから足すのはどちらがいい?
なるほど。
例えば、3~5を足した平均、というのは、数式で書けば
(3+4+5) / 3
となる。3つの数を足しているから、3で割る。
でも、分配側を使えば次のようにもかける。
3/3 + 4/3 + 5/3
数学では、この2つは同じものだ。
彼は直感的には最後に1度だけ割り算をした方が良さそう、と思ったそうだ。一応 Pascal をやっていて、丸め誤差などの存在は知っていた。
でも、実際の所どうなのか、と思って質問してきたのだった。
僕の答え。
最後に割り算、というので基本的にはあっている。割り算は誤差が生じるし、コンピューターの処理としては重い処理なので、できるだけ少ない回数で済ませたい。
ただし、「桁あふれ」が生じそうなときは、あらかじめ割る場合もある。
例えば、「銀河系の星の地球からの平均距離(km)」を知りたかったとしよう。
1つの星への距離も大きな単位で、これがまさに星の数ほどある。「先に全部足す」などをすると、桁あふれを起こす。このような場合は、溢れない程度の数ごとにまとめ、グループ内の平均をとった上で、その平均をさらに平均化する…というような処理になるだろう。
しかしまぁ、そんなのは特殊例。基本としては割り算は最後、でいい。
n / 3 / 4
みたいに何度か割り算をする場合は、 n / (3*4) というように、割り算が減るように変形したいところ。
今のコンピューターは十分速くなっているのであまり気にしないでよいのだが、それでも割り算処理は重い。
▼if の後に1命令しかないとき、 { } はつけるべき? つけないべき?
案外知られていないが、構造化言語の文法では、if は「続く1命令を」実行するかしないか、という機能しかないことになっている。
しかし、現実には1命令ではなく、ひとまとまりの命令を制御したいし、ちゃんとできている。
そこで出てくるのが「複文」という概念だ。複数の命令を { } で括ると、それ全体が1つの命令としてふるまう。
(彼は言語の教科書を読んで勉強中なので、ここまでは正しく理解していた)
そして疑問となる。1命令しかないときに、わざわざ複文を作るべきか否か。
これは、完全に個人のルールだろう。僕が作る場合も、状況により使い分けている。
会社によっては、プログラムが俗人的になるのを嫌い、「必ず { } をつける」などのルールを定めている場合もある。
たとえば、以下のようなプログラム
if(x< 0) x= 0; if(x>255) x=255; if(y< 0) y= 0; if(y>255) y=255;
僕はこの場合には { } を付けない。つけない方が見やすいからだ。
でも、1命令でも { } をつけることはある。
if(longFlagVariableName){ longReciveVariableName = longFunctionName(longParameterVariableName); }
ここで { } を付けたいと思う理由はいくつかある。
まず、if の後ろで改行したいため。改行の理由は、変数や関数の名前が長いから。
改行しないと、エディタの1行に収まらずに読みにくくなるかもしれない。
改行するなら、明示的に「どこまでが if の影響範囲か」を示したい。
次に、関数呼び出しを含んでいるため。
上手く動かないときに、デバッグのために、「関数を確かに呼び出してる」「呼び出した変数の値」「受け取った変数の値」などを、プリントデバッグするためのコードを追加したくなるかもしれない。
こうした可能性を考えて、最初から { } で複文にしちゃう。
つまりは、ここら辺は「論理」ではなくて、長いプログラマー生活の経験則、勘だ。
こういうものは、本やネットに書かれていない、というのは非常によくわかる。
▼ループ変数は、ループの外で使っても良いの?
こんな感じのプログラムを見たらしい。
for( i=1; i<=10; i++ ){ sum = sum + i; } print("sum(1~%s) = %s", --i, sum);
1~10 を足して、 sum(1~10) = 55 と表示するプログラム。
…なるほど。10 ではない数までを足したいとき、for の最後の条件を変えても、正しく動く。
彼はこのプログラムが気持ち悪い、という。
まぁ、僕もこういうことはしないだろう。最後の数(10) を変える場合を想定するのであれば、そこを変数にする。
そしたら、外側でループ変数の最後の値を使わないでも、「終了条件の値」の変数を使えばいい。
しかしまぁ、これも程度問題。ループ変数をループ外で使ったって別に問題はないし、状況によっては悪くない使い方ができるかもしれない。
ただ、ループ変数をループ外で使うのは、無用の混乱を招きやすい。
つまりは、「バグの元」という意味だ。
そこで、最近の言語ではこんな書き方もできる。
for( let i=1; i<10; i++ ){ ~ }
最近の javascript 等では、上のように for のループ初期化の際に let で変数を定義できる。
すると、この変数は定義されたループの中でしか使えない。
そうすることで、ループ変数をループ外で使用する、という「無用の混乱」を避けられるのだ。
#let は、先に書いた「複文」を示すブロック内で使うと、そのブロック内でしか参照できない変数になる。
しかし、for は少し特殊で、「複文」の外側にある、for の条件定義部分で let を使った場合でも、ブロック内の定義と見なされる。
▼パスカルの三角形を生成する、という例題があった…
質問が長かったので、解説しながら書く。パスカルの三角形とは次のようなもの。
1 1 1 1 2 1 1 3 3 1 1 4 6 4 1
上の段の数字の「間」に、上の段の2つの数字を足したものを書く。両端は 1 にする。
これを計算で作りだせ、という例題があったのだそうだ。
この時、「両端は1に」というのが、特殊な処理になる。
以下、1行の数値を配列に入れて処理している、という前提で話を進める。
1) 両端を含めたループを作り、ループ内で if を使って両端を認識、1 を出力する。
2) 両端を取り除いたループを作り、ループの前と後ろで 1 を出力する。
3) 両端を含めたループで処理はするのだが、両端は正しく処理できないので、後で 1 で上書きする。
彼がネットなどで調べたら、上記の3つのパターンがあったらしい。
どの方法でも結果は出せるのだが、どれを使うべきだと思う? と問われた。
僕の答え。
まず、3 は論外。「うまく処理できない」というのが、ここでは配列の「まだ初期化していない部分」をアクセスしてしまって計算結果が不定、というようなものだったらしい。
ここで、両端を1にすることで、「次の処理の」ための数値は初期化される。
まぁ、動くことは動くかもしれないが、初期化していない変数にアクセスすべきではない。
また、計算した結果を後で上書き、という無駄をするのであれば、計算しない方が良い。
1 は、初心者向けにはわかりやすいと思うが、両端の処理のためだけにループ内に if を持ち込むのであれば、持ち込まない方法を考えた方が良い。
そして、if を持ち込まない、と考えると、答えは自然と 2 に落ち着く。
彼としては、計算がループというブロック内に収まらず、その前後に処理がある、というのに「美しくない」印象を受けていたようだが、どんな処理にも前処理と後処理はあるものだから、例外部分をそこに寄せていると思えばよい。
これは、ループ内は何度も処理されるので、処理が軽い方が良い、という単純な理屈だ。
ここで「ループの影響を受けない計算は、ループ外に追い出すと良い」という話をした。これは最適化の話で、初心者のうちはあまり考えないでよい。でも、気になり始めたらすでに中級者なので、知っておいてよいと思う。
▼今聞いてきたような話、少しでも効率よくなるように書くべき?
これが最後の質問。
最初は気にしない方が良い。効率化は最後に考えるもの。
もちろん、慣れてくればある程度効率的な方法で書いたり、後で効率化しやすい方法で書く、ということはできる。それでも、最初から効率化しない方が良い。
プログラムは必ず仕様変更するもの。
「ある状況」を考えて最適化されたプログラムは、別の状況になると全く役立たずだったりする。
効率化されていないプログラム、というのは、ある意味では「シンプルで変更しやすい」プログラムだったりもする。
僕がプロとして心がけているのは、動くものを早めに作ることだ。
まず確実に動けば、処理が遅いとか、問題点もわかってくる。早めに仕上がっていれば修正の時間はあるので、そこから効率化を考えればよい。
最後に、Cのようなコンパイル言語では、ある程度の効率化は勝手にやってくれる、という話もした。
最近は Javascript でも、書いたプログラムを直接ブラウザで動かす、ということは減っている。Javascript から「効率の良い Javascript」へと変換する、トランスパイルと呼ばれる作業を入れることがあるのだ。
もちろん、アルゴリズムから効率化されているプログラムは高速に動く。
しかし、小手先の効率化くらいであれば、コンパイルなり、トランスパイルなりの際に自動的にやってくれることも多い。
以上。
いずれもたわいもない話だし、正解はない。でも、確かに中級者のころは、僕もこうした部分でいちいち躓いていた気がする。
ネットにはあまり情報がない、という話だったので、あえて日記として残してみる次第。
初級者から中級者にステップアップする際に、こんな話は、他にもいくらでもありそう。
同じテーマの日記(最近の一覧)
別年同日の日記
19年 ファミリーベーシック V3(1985) ディスクシステム(1986)の発売日
申し訳ありませんが、現在意見投稿をできない状態にしています。 |