プログラミングの基本的な考え方 - HondaLab/Robot-Intelligence GitHub Wiki
コンピュータープログラム=(変数+式)x(反復+分岐)
コンピュータープログラムの基本は,順番に式を使って変数の値を計算するということです. 順番に計算するだけでは単純なことしかできませんが,それを反復(くりかえし)したり,条件による分岐 させることで,より複雑な処理を構成することができます. (構造化プログラミング)
作り上げた処理をひとかたまり(関数やサブルーチンなど)にして名前を与えて,それをまた反復したり 分岐したりすることで,さらに複雑な構造の処理を構築していくことができます.
ご存知の通り,変数や関数という用語は,数学で現れる言葉です. プログラミングにおける「変数」や「関数」は,厳密には数学の概念とは異なりますが,似たような働きをします.
変数=名前
と思って,さして問題はありません. 最初に数学変数を学んだときは「値を代入する箱みないなもの」というふうに習ったかもしれません. その通りなのですが,プログラミングにおいては「変数=値につける名前」と思っておいたほうが考えやすいと思います.
もちろん名前ですから,数学でよく出てくる "a" や "x" などという意味不明の名前をつけるよりも,"Left_Sensor"といったように意味が理解しやすい 名前をつけたほうがアルゴリズムの見通しはぐっと良くなります.
Pythonなどのオブジェクト指向におけるクラスやインスタンスといった概念も「名前の拡張版」だと最初は理解して差し支えありません. むしろ「インスタンス」などというコンピュータ用語を振り回すよりも,
処理の流れを理解しやすい名前
をつけることを念頭に置いたほうが,正確になおかつ,楽に目的を達成できるでしょう.
関数=処理のかたまり
です.たとえば
f(x) = a*x^2
という関数 f(x) は「変数xを2乗して,それにaを掛ける」という処理のかたまりを意味しています. その処理のかたまりに f という名前をつけたのです.
また名前という言葉が出てきましたね. ここでは,xやfという名前を使う例を示しましたが,必ずしもこの様な名前付をする必要はありません.
むしろ,
なるだけその内容を表すわかりやすい名前をつけましょう.
日本では,中学校の数学の授業で最初に関数を f という記号,変数を x という記号を使って習います. そのため最初はついつい,関数や変数として f(x)や g(y)などのような単純なシンボルを使いたくなります.
たしかに簡潔なシンボルは概念の関係性や処理のながれを明確に表すには良い方法です. しかし,そのシンボルが何を意味しているかをあとから思い出すのは難しいです.
タッチセンサーを x,右のモーターを y とするよりも
タッチセンサーを ts, 右のモーターを mR と名前をつける
方が,そのシンボル(変数など)が何を意味しているのか思い出しやすいのです.
些細なことの様にみえますが,実はこの「意味を思い出しやすい名前をつける」ことが,プログラムの可読性や 継続的なメンテナンスにおいては本質的であり,なおかつ重大な意味を持ちます.
プログラム=コンピュータの中で動き回る変数や関数
と言えばわかりやすいかもしれません. 繰り返しますが,変数や関数にはその意味するところをイメージしやすい名前をつけましょう.
紙の上の変数や関数が,それ自身の値が代入されたり,値が変化したりする,つまり「動き回る」のは人間の頭の中です. つまり数学記号は思考の対象を具体化したり,その処理過程を助ける役割を持っています.
いっぽうプログラムの中の変数や関数は,コンピューターという機械のなかでその値を変えたり呼び出されたりします. つまりプログラムは人間の頭脳の中だけではなく,機械の中でも実際に動き回ります. それ故,「人工知能」とよばれるものの実態は,ほとんどがコンピュータープログラムのことです. つまり,
人工知能(AI) = コンピュータープログラム
と思っても,それほど大きく違いはありません.
では,なにが変数や関数の「うごき」をきめているのか というと,基本要素は
「反復」と「条件分岐」です.
どんな複雑なプログラムもこの「反復」と「条件分岐」を組み合わせて構成されています. その処理の流れのことを「アルゴリズム」と言います.
たとえば,先の例にあげたアルゴリズムを具体的なPythonコードにしてみましょう.
import ev3dev.ev3 as ev3
import time
ts=ev3.TouchSensor('in1')
us=ev3.UltrasonicSensor('in3') # 超音波センサーに us という名前をつける.
mL=ev3.Motor('outA')
mR=ev3.Motor('outD')
mL.reset()
mR.reset()
while ts.value()==0: # タッチセンサーが押されるまで反復する.
mL.run_direct(duty_cycle_sp=+50)
mR.run_direct(duty_cycle_sp=+50)
time.sleep(0.1)
distance=us.value()
if distance<300: # 条件分岐
mL.run_direct(duty_cycle_sp=+50)
mR.run_direct(duty_cycle_sp=-50)
time.sleep(1)
mL.stop()
mR.stop()
「while ts.value()==0:」の部分が反復の開始です. 反復は無限にくりかえすわけにはいかないので,くりかえされる条件を「ts.value()==0」 と記述してあります. 実行される条件ですので,停止したい条件の逆(否定)と考えたほうが理解しやすいかも しれません.
このようにwhile文は,くりかえしと条件分岐の両方を一度に記述できる, アルゴリズムを明瞭にするうえでとても便利な文です.
「if distance<300: 」の部分が条件分岐です. このように条件分岐することで「障害物が300mm以下の距離に近づいたら,右に1秒間曲がる」という 行動をプログラムで実現することができます.
もちろん,複数の条件分岐を並列的に並べたり,条件分岐の中に条件分岐を入れ子にして,複雑な動きを 実現することができます.
アルゴリズムの核心部分は意外とシンプル
です. 殆どは例外処理などそれ以外の付随的な部分にたくさんのプログラムコードが費やされているケースが多いです.
例外処理などは大事ですが,その重要性はアルゴリズムの根幹部分に比べたら2の次,3の次です. 大事なことはあなたのつくるプログラムのアルゴリズムが,意図する目的を正しく達成することです.
より複雑なプログラミングを行うためには
関数の合成を思い出すと良いでしょう. 例えば
h(x) = g(f(x))
という関数h(x)は,z=g(y)とy=f(x)という2つの関数を合成した関数になっています. すなわち,プログラミングにおいても,関数で求めた結果を,別の関数に用いるということを 繰り返して複雑な機能を実装します.
これが,「複雑な機能を単純な部分的機能に分解して,それらを最終的に組み合わせる」という 考え方の具体的な一例のイメージです.
もちろんこれで足りなければ,さらに x を求める関数,x=p(a) をつくれば良いのです. あるいは,h(x)を使って,さらに別の機能(output値を求める)をつくりたければ,
output=h2(h(x))
のように,関数をさらに合成すれば良いのです.
このように,単純な処理をいくつも合成(組み合わせ)して,最終的に
a --> output
という複雑(に見える)機能を実装します.