高校数学の窓過去問検索

方程式の近似解

さて、今まで二節を使って「再帰の練習」をしてきました。いや、事実上これらは「再帰の練習」がトピックだったのです。(そして、その再帰ネタを「反復で行う」と言うネタです)
元々、このテキストの背後にあるネタを整理してみると、


  1. テキスト後半の著者である菅原昭博・現玉川大学工学部電子工学科准教授は東工大出身である。

  2. 東工大出身である以上、設計理論上、再帰方程式使いまくりの関数型プログラミング言語、Lispに精通している。

  3. プログラミング未経験の学生に数値計算を迅速に教えるのはLispが最適である。

  4. しかしながら、90年代初頭のパソコンでは非力過ぎてLispは動かなかった。

  5. しょーがないので、Lispで扱うネタを当時それなりに流行っていたPascalを模した「擬似プログラム」で解説するしか無かった。

  6. だから解説がワヤクチャである。


と言う事です。実際、これまでの2節の内容は、Lisp/Schemeの教科書の「最初にやるネタ」としては良くあるトピックなんです。
(一方、当然ながら、このテのトピックは「Pascalの入門書」とか「Cの入門書」には出てきません)

プログラミングは難しい?確かに難しいかもしれません。
しかし、このテの数値計算が「余計難しく見える」最大の理由は、

初心者にとって、「数学的アルゴリズムが難しい」のか、「プログラミング自体が難しい」のか判断が付かない

からなんです。難しい×難しい、だと混乱して当たりまえ、なんですよ。
ですから、通常、「数値計算」ってトピックは「一通りPascal(あるいはC言語)を扱えるようになってから」別の教科書で学ぶ、となってるんです。
そこで当然方策としては「数学的アルゴリズムが難しい」から「プログラミング自体が難しい」を切り離してしまった方が良い、って事になります。単純に「簡単なプログラミング言語を使えば問題の半分は解決する」と言う方法です。
これは成功した、と思います。Python、Ruby等の「動く擬似コード」の導入によって「数学的アルゴリズムが難しい」、だけを意図的に切り離したんです。
今まで、演習問題の回答例を見ていて

「本体がたった2〜3行程度、でプログラムと呼べるのか?」

とか思った人もいるかもしれません。が、実は「2〜3行程度でも」れっきとした「プログラム」です。
と言うか、プログラムは「短く書ければ書けるほど良い」んです。そして、プログラミング言語自体が強力になればなるほど「短く記述する事が」可能になるのです。
これが90年代初頭と今現在の「テクノロジーの差」なのは言うまでもありません。

Python、Ruby、Rはどれも「新しい」言語です(例外はSchemeだけ、なんですが、そもそも背景が数学である以上「古くは」なりません)。プログラミング言語の選択も、原則的には「なるべく新しい言語を選んだ方が良い」と言う事です。
これはテクノロジー選択の原則で、例えば今手元に10万円程度あるとして、何か録画機器を買いたい、とします。ビデオテープレコーダー、DVDデコーダー、ハードディスクレコーダーとあった場合、まあ、十中八九「ハードディスクレコーダー」を買う人が多いでしょう。何故なら「新しいから」ですよね。今更ビデオデッキを買おう、って人はあんまりいないでしょう。何故なら、テクノロジーは基本的には「新しい方が良い」からなんです。プログラミング言語もテクノロジーの範疇である以上、同様の文脈となります。
ここで始めてPython、Rubyなんかのプログラミング言語の存在を知った人も多いと思いますが、プロの現場でも愛用されている、と言うのは先ほど書いた通り「記述能力が高い=短くコードを書けるから」なんです。短ければ短い程「間違い」が混入する確率も低くなりませす。そして、この二つは「Webアプリケーション記述」が特に得意な分野なんで、実は知らないうちに「Python/Rubyで書かれたサイト」を見ている可能性もあります。つまり、あなた方が考えている程「マイナー言語」ではないのです。
また、Rは実際「統計解析の分野」で良く使われているソフトウェアなんで、ここでも「記述が長いと」日常的に使うのがかったるくなるんで、やっぱりなるべくシンプルに書けるように設計されています。これも特殊分野ですが、その分野ではやはり「メジャー言語」なのです。
これらの言語の隆盛は、一つ、コンピュータの処理能力が劇的に上昇した事に由来しています。これらも一昔前だったらマトモに動かすのが大変だったでしょう。しかし、今の標準的なコンピュータだと、CPUの演算速度は最低でも1GHz突破してるでしょうし、メモリにしても1Gb近くは搭載しているケースが多いと思います。既に昔の大型コンピュータの能力を凌駕してるんで、こう言う「記述が短く簡単なプログラミング言語」が登場してきたのです。
ちょっと長くなりますが、大事な事が示唆されている文書があるんで引用してみます。


100年後にコンピュータが何で作られていようが、現在のものより遥かに速いと考えるのは安全な予測だろう。ムーアの法則がこのまま続けば、コンピュータは現在の 7400京倍(73,786,976,294,838,206,464)の速度を得ることになる。想像し難い数字だ。実際、速度の面ではムーアの法則は破れるだろうと考える方が無難な予想だ。18ヵ月毎に2倍になるなんてものは、それが何であれ、いずれ根本的な限界に達しそうだと考えられる。そうだとしても、コンピュータが現在のものよりずっとずっと速くなると考えて良いだろう。たとえそれがたかだか100万倍であったとしても、プログラミング言語の基本的な法則を大きく変えるには十分だ。その中には、例えば現在では遅いと考えられている言語、つまり効率的なコードを生成しない言語の活躍の余地が大きくなるということが挙げられる。

もちろん、依然として速度を要求するアプリケーションもあるだろう。コンピュータを使って解きたいと思う問題のいくつかは、コンピュータ自身によって作られる。例えば、コンピュータで生成されたビデオイメージの処理は、生成側のコンピュータの速度に追い付いていなければならないだろう。さらに、いくらマシンサイクルがあっても本質的にそれを喰い潰してしまう類の処理というのがある。イメージレンダリング、暗号、シミュレーションといったものだ。

いくつかのアプリケーションはますます非効率になって行く一方で、ハードウェアの限界までの速度を要求するアプリケーションがあるとすると、速い計算機を手にするということは、言語は効率においてより広い範囲をカバーしなければならなくなるということだ。既にそれは起こりつつある。人気のある新しい言語の現在の実装は、10年前の基準では驚異的に非効率だ。

これはプログラミング言語だけに起こっていることではない。一般的な歴史の流れだ。技術が進むにつれ、各世代はその前の世代だったらもったいないと思っていたようなことを平気で出来るようになる。30年前の人は、我々がこんなに気軽に長距離電話をかけるのに驚愕するだろう。100年前の人は、ボストンからニューヨークまで翌日配達便で荷物を送るのに、メンフィスを経由すると聞いてひっくり返るだろう。

次の100年の間に、速いハードウェアによって我々が得る余分なマシンサイクルがどう使われるかを当ててみようか。そのほとんどは無駄に使われることになる。

私は、コンピュータの力が貴重であった時代にプログラミングを学んだ。 4KバイトメモリのTRS-80に載せるために、Basicプログラムから全ての空白を抜いたことを覚えている。同じ作業を何度も何度も繰り返すのにマシンサイクルを喰いつづける、おそろしく非効率なソフトウェアのことを考えるのと気分が悪くなる。だが、ここでは私の直観は間違っているんだろう。貧乏が身についてしまうと、例えば医者にかかるといった重要なことに金を使うのを躊躇してしまうようなものだ。



ここでは、マシンが速くなればなるほど工学的な意味での「効率的なプログラミング言語」より「非効率な」しかし「記述能力が高い言語」の重要性が増してくる、と言う事が語られています。つまり、遅いマシンだと「機械に気を遣って」人間側が機械により近い立場の言語を使って「効率的な」プログラムを書かなければならないのですが、今みたいに(また将来的に)マシンが十分な速さを持つと、その重要度の比率は逆転してくる。現代の状況では「記述力の高い言語」を扱うのには十分だ、と言う事が出来るんです。
通大のテキストのように「90年代の常識で」「数値計算を語る」プログラミングの教科書の存在、ってのは本当に害悪なのです。しかも相手は「数学教員免許取得希望者」であって「情報工学専攻」じゃない。少なくともPython、Ruby等の「現存するプログラミング言語のうち、最も簡単な言語」さえそれなりに扱えれば十分、です(もちろん、これさえ扱えない、となればそもそも教員免許取得の能力が無い、のと同義でしょう)。
まあ、別にストイックに「C言語等で効率的なコードをガリガリ書くのも重要」ってのは否定しませんが、今の時代だとそれは「情報工学専門」向けです。そう言うメンド臭さは、繰り返しますが、「数学教員として」必ずしも求められているもの、とは違うのです。
(ホント、さっさとテキスト改訂するべきです)

と言う辺りで前フリを終わっておいて、この節からは本格的な「数値計算」が始まります。
今、僕等の手の中にはPython、Ruby、Scheme、Rと言った動的型付け言語のうちのどれかがあります。そして「再帰」と言う武器もあります。従って、現時点では「プログラミングの面倒なお約束」は「数値計算」自体のトピックとは完全に切り離された状態です。
通大のテキストも「数学的背景」の説明が前半、実装に付いてのアイディアが後半、と完全に切り離されていますね。これは非常にやりやすい、です。
前半の「数学的説明」は「その通り」なんで、それはキチンと読んで理解して下さい。ここは「プログラミング」ではなくって単なる「数学」です。重要な部分は下線を引いていきます。それが後半で「実装する」際のヒントとなるでしょう。
では、始めます。


関数が$f(x)=x^2+2x+1$のような場合は、方程式$f(x)=0$を代数的に解くことができますから、あえてコンピュータを用いてその解を計算する必要はありません。しかし、このように容易に解を求めることができる方程式ばかりではありません。現実には解の存在は分かっているのですが、それをきちんと求めることができない方程式に出会うことのほうが多いのです。ここでは、そのような方程式に対していかなる方法で、その解を見つけるか考えてみましょう。
次にその解決策を2通り説明します。




  1. 2分法
    解析学の基礎を学んでいると、次の連続関数に対する重要な定理が必ず出てきます。

    [定理] 関数f(・)は閉区間[a, b]において連続とする。f(a)、f(b)が異符号ならば、方程式f(x)=0は開区間(a, b)において少なくとも一つの解を持つ。

    これは有名な「中間値の定理」の一つの系です。また、これが証明されれば「中間値の定理」はこの定理の系として証明されます。
    この定理の証明の仕方はいくつかありますが、その中に解を具体的な手順によって求めていくものがあります。

    (証明) まず、f(a)<0、f(b)>0の場合に証明しましょう。いま、a0=a、b0=bとおき、m0=(a0+b0)/2を求めます。ここで、f(m0)=0ならば、このm0が求める方程式の解となります。それ以外の場合、


    f(m0)>0ならば、a1=a、b1=m0
    f(m0)<0ならば、a1=m0、b1=b


    とし、m1=(a1+b1)/2を求めます。このとき、f(m1)=0であればm1が求める解となり、そうでないときは、


    f(m1)>0ならば、a2=a1、b2=m1
    f(m1)<0ならば、a2=m1、b2=b1


    として、m2=(a2+b2)/2を求めます。この操作を有限回繰り返して、f(mk)=0となるmkが得られば定理は証明されたことになります。そのようなmkが存在しないときは、無限の繰り返しによって、


    a=a0≦a1≦a2≦・・・≦b2≦b1≦b0=b


    となる、有界な単調列{ak}と{bk}が得られます。このとき、


    $b_k−a_k=(b−a)/2^k$


    が成立しますから、この2つの数列は同じ極限値cを持つことがわかります。ところで、数列の作り方から


    f(ak)・f(bk)<0


    となっています。ここで、k→∞とすれば関数の連続性から、f(ak)とf(bk)はf(c)に収束しますから、


    f(c)・f(c)≦0


    となり、したがってf(c)=0、すなわちcが方程式の解となります。
    f(a)>0、f(b)<0のときは、関数−f(・)に、今の手順を適用します。///



    この定理の証明の中で使われた方法をもとにして、方程式の近似解を求める手法を2分法と呼びます。その名の通り、最初に与えられた区間の2等分を繰り返し、目的の解に近づいていきます。



  2. ニュートン・ラフソン(Newton-Raphson)法
    関数f(x)の導関数f'(x)が分かっている場合は、方程式f(x)=0の解に対して次の定理が成り立ちます。この定理の中の手順による反復計算によって、解を求める方法をニュートン・ラフソン法と呼びます。

    [定理]  関数f(x)は、閉区間[a, b]で2回連続微分可能とし、

    1. f(a)とf(b)は異符号

    2. a≦x≦bで、常にf'(x)>0またはf'(x)<0

    3. a≦x≦bで、常にf"(x)≦0またはf"(x)≧0

    4. 端点aとbで、|f'(x)|が大きくない方をcとしたとき、

      |f(c)|≦(b−a)|f'(c)|



    を満たすとする。このとき、区間[a, b]の中の任意の点x0から出発して、

    xn+1=xn−f(xn)/f'(xn)

    により定められた数列{xn}は、方程式f(x)=0の唯一の解に収束する。

    この定理で1.の仮定から前の定理を使って開区間(a, b)の間に少なくとも1つ解があり、2.からそれが唯一であることがわかります。3.は関数のグラフがその区間で上に凸であるか、下に凸であるかを示しています。4.の仮定は|f'(x)|が最小になる端点におけるこの関数の接線が区間[a, b]でx軸と交わることを意味します。

    (証明)  ここでは次の場合について証明します。

    f(a)<0、f(b)>0、f'(x)>0、f"(x)≧0、c=a

    他に3つの場合が考えられますが、すべてこの場合に帰着されます。
    方程式f(x)=0の唯一の解をsとおくことにし、点x0をs≦x0≦bであるとします。(x0, f(x0))における接線


    y=f'(x0)(x−x0)+f(x0)
    x1=x0−f(x0)/f'(x0)


    となり、f'(x0)≧0、f(x0)>0からx0≧x1が成立します。一般に、


    x0≧x1≧x2≧・・・≧s


    が成り立つことを帰納法で示します。xn≧sならば、平均値の定理より、


    f(xn)=f(xn)−f(s)
       =(xn−s)f'(γ)  (s≦γ≦xn)


    となります。ここでf"(x)≧0から、


    f(xn)≦(xn−s)f'(xn)


    となります。したがって、


    xn+1=xn−f(xn)/f'(xn)≧s


    が成り立ち、f(xn+1)となります。単調減少であることも。上の式からあきらかです。
    下に有界な単調減少列は極限を持ちますから、それをαとすればs≦αとなります。fとf'の連続性から、数列を作り出す式において、n→∞とすれば、


    α=α−f(α)/f'(α)


    からf(α)=0です。ここで、方程式f(x)=0の解の一意性からα=sとなります。///

    以上の2つが、方程式の解を求める方法の数学的基礎となるものです。これを基にして、各方法のプログラムを作成します。





  3. 収束の判定
    ところで、上に書かれたことをプログラミングするにあたって一つ問題があります。反復により生成される数列{xn}の収束を一般にどのように扱えばよいのかということです。コンピュータがいくら高速の繰り返しを得意としても、いかなる処理も無限に行うことはできません。コンピュータを使うのは、その高速性を活かして短時間で求める結果が欲しいからです。無限の時間をコンピュータに与える訳にはいきません。
    コンピュータによる実数型の計算は、近似計算であることを思い出してください。今のように方程式f(x)=0の解を求める場合、その解を求めるための反復計算や関数の値を計算するときに計算誤差が入り込んできます。そこで、

    「あらかじめ許容される誤差εを与えておき目的の解の値がその誤差の範囲内に納まれば、すなわち


    |xn−xn+1|<ε


    であれば収束したとみなす」

    または、

    「方程式の解における関数値を計算するときの計算誤差δを、前もって見当をつけておき、


    |f(xn)|<δ


    となったら収束したものとみなす」

    のどちらかの判定基準を用いて反復を終了し、その時点におけるxnを解とするのが一般的です。当然、近似値となります。




[課題3・1] 関数f(・)が与えられているとき、方程式f(x)=0の解を2分法により求めよ。

まず、関数f(x)を計算するサブプログラムはすでにあるものとして、次のようにプログラムを分解します。

  • 出発点[a, b]の左端aと右端bを入力する

  • 上の区間に2分法を用いてf(x)=0の解を求める

  • 解を出力する


1番目の入力では、このaとbが適当なものであるか判断しなくてはなりません。2分法では、出発区間の左端aと右端bにおける関数の値が異符号でなければなりません。それを考慮して、さらに次のように詳細化します。

  • 2つの実数a、bを読み込む

  • もし、f(a)f(b)>0 ならば プログラム終了


ここで、「f(a)f(b)>0」というのは、f(a)とf(b)は同符号であるかの判定を行うためのものです。共に正であるか共に負であるか別々に調べなくとも、この積の正負だけで必要な判断を行うことができます。
2番目の命令をどのように詳細化するかが問題です。ここで、2分法をもう一度簡単に説明しましょう。
左端akとbkで異符号の関数値を持つ区間[ak, bk]を、その中点mkで2つの区間[ak, mk]と[mk, bk]に分けます。f(mk)≠0ならば、このうち少なくとも一方の区間で、左端と右端の関数値が異符号となります。その区間を[ak+1, bk+1]とします。これを繰り返して得られる無限数列{ak}、{bk}は、共に方程式f(x)=0の解に収束します。
ここでは、前もって与えたεを使って|ak−bk|<εで収束と判定することにします
いままでの説明から、2分法は次のような手順で実行すればよいことがわかります。ただし、区間[a, b]の中には方程式の解が存在し、変数mはその中点を意味します。また、tは一時変数で次の二つの判断で使われます。最初の判断では、f(a)とf(m)が同符号になりますからmを新たにaとします。次の判断ではf(a)とf(m)が異符号になりますからmを新たにbとします。まず、f(m)=0となることはありませんが、もしそうなった場合でもa=bとなり反復は終了します


  • |a−b|≧εである限り次を繰り返す
    {

    • m←(a+b)/2

    • t←f(a)×f(b)

    • もし t≧0 ならば a←m

    • もし t≦0 ならば b←m


    }



この反復が終了した時点の変数aあるいはbの値を、方程式の解とします。
以上をまとめて、プログラムは次のようになります。ここで、誤差として10-6をとってみました。


  • プログラム  「二分法」

    • 定数 ε=1.0E-06 : 実数型

    • 変数 a、b、m、t : 実数型

    • {
    • a、bの値を読み込む

    • もし f(a)×f(b)>0 ならばプログラム終了



    • |a−b|≧εである限り次を繰り返す
      {

      • m←(a+b)/2

      • t←f(a)×f(m)

      • もし t≧0 ならば a←m

      • もし t≦0 ならば b←m


      }

    • a (またはb) の値を出力する

    • }






下線部:亀田


さて、特にツッコミもなく、ここまで至りました。と言うのも、殆どが「数学の話」ですんで「その通り」としか言いようがないのです。
また、今回は、上の「疑似プログラム」にも敢てツッコんでません。これも後で自分でwhile文で実装してみる、ってのも良い練習だとは思うから、です。
しかしながら、ここでは2分法も「再帰の枠組み」で記述することにします。と言うのもそっちの方が「簡単だから」ですよね。

ところで、上の「疑似プログラム」を見ると、条件節が入れ子になっていて、あまり美しくありません。入れ子が必要になる時はなる、んですが、不必要に入れ子になるとコードの流れを把握するのが一般的には難しくなるから、なんです。
また、下線部引いてきたところと、上の疑似プログラムの流れを見ると、実はおおまかに言うと「次の事が」プログラムの流れになるのです。


  1. プログラムBisectionMethodの引数はf、a、bの3つとする。→表記は取り合えずここではBM(f, a, b)とする。

  2. 計算誤差εを$$\epsilon=10^{-6}$$とする。

  3. 中間点mをm=(a+b)/2とする。


    • 条件1:もしf(a)×f(b)≧0だったらプログラムはエラーを返す。

    • 条件2:もし|a−b|<εだったらaを返す。

    • 条件3:もしf(a)×f(m)>0ならばBM(f, m, b)を呼び出す。←再帰!!!

    • 条件4:それ以外の場合、BM(f, a, m)を呼び出す。←再帰!!!




だいぶ圧縮されましたね。再帰は「分岐」ですし、「反復が分岐で書ける」のなら、結果全部分岐なんで入れ子にする必要が全く無い、と言う事なのです。
注釈を少々。
まず、上の疑似コードの大本の1.を見てほしいんですが、通大テキストでは入力値はa、bの二つの実数だったんですが、上の疑似コードではf、a、bと三つに増えています。a、bは同じで良いとしてfとは何だ???
fは「関数」です。つまり、ここでは、適当な関数fを引数として与えて、その解を2分法で解けるようにしよう、としているわけです。
ここで、通大「コンピュータ」テキストのp144辺りを見てほしいんですが、Pascalによる2分法のコード例が書かれています。これ見ていると中にx*x*sin(x)と書かれた部分があるでしょう?
実はこの部分は2分法のロジック自体とは全く関係無い部分だと言う事です。つまり、このコードはx*x*sin(x)と言う関数「だけ」を2分法しようと言ってるんですね。
ちょっと待てよ……って事はx*x*sin(x)と言う関数以外を2分法する場合は?答えはその部分を書き換えて使わないとならないと言う事を示しているのです。
しかもPascalは一般的にはコンパイラ型の言語なので、ソースコードを書き換えては一々コンパイルせねばならない……要するに汎用性としてはイマイチ、なんです。シチメンド臭いですね。
だったら任意の関数を引数に与えて、それを2分法で解いた方が面白いし便利だろ、って事です。従って、ここでは引数fを増やして2分法を「任意の関数f」に適用する事とします。
次に条件3と条件4ですが、これらは通大テキスト「疑似プログラム」繰り返し内のa、ないしはbへのtの代入と同じ行為をしています。「疑似プログラム」では変数への代入だけ、ですが、再帰版では再帰の肝、「今作成している関数自体の引数を変更して呼び出す」スタイルになっています。

Pythonによる2分法





# -*- coding: utf-8 -*-

def BisectionMethod(f, a, b):

eps = 1.0E-06
m = (a + b) / 2.0 # ここに注意

if f(a) * f(b) > 0:
raise ValueError
elif abs(a - b) < eps:
return a
elif f(a) * f(m) > 0:
return BisectionMethod(f, m, b) # ここで再帰
else:
return BisectionMethod(f, a, m) # ここで再帰





再帰部分はまあ、良いですよね。
Pythonのコードでの注意点はm = (a + b) / 2.0の部分です。ここを決してm = (a + b) / 2と記述してはなりません
と言うのも、Pythonの場合、整数/整数と言う表記をすると解も整数になる、と言う性質があるから、なんです(これは他の言語、例えばC言語なんかにも見られる性質です)。正確に言うと、余りを切り捨てて商だけ返してしまうのです。
そうすると中点が必ず(中途半端な、しかも実際は中点ではない)整数として計算されて、収束計算が終わらず無限ループに突入してしまうのです。それを避ける為には、割る方の数、この場合は2ですが、これを実数型として認識させる為にあえて小数点以下を記述して2.0としなければならないのです(もちろん、与える引数を必ず実数にする、って回避法もあるんですが、メンド臭いでしょう)。
この辺は一回はハマるので気をつけて下さい(僕も良くハマります・笑)。

Rubyによる二分法





def BisectionMethod f, a, b

eps = 1.0E-06
m = (a + b) / 2.0 # ここに注意1

if f[a] * f[b] >= 0 # ここに注意2
raise
elsif (a - b).abs < eps # ここに注意3
a
elsif f[a] * f[m] > 0
BisectionMethod(f, m, b) # ここで再帰
else
BisectionMethod(f, a, m) # ここで再帰
end
end




再帰部分はまあ、良いですよね。
2分法でのRubyのコードでは注意点が全部で3つあります(ちょっと多いですね)。順番に説明していきます。
Rubyのコードでの注意点その1。m = (a + b) / 2.0の部分です。ここを決してm = (a + b) / 2と記述してはなりません
と言うのも、Rubyの場合、整数/整数と言う表記をすると解も整数になる、と言う性質があるから、なんです(これは他の言語、例えばC言語なんかにも見られる性質です)。正確に言うと、余りを切り捨てて商だけ返してしまうのです。
そうすると中点が必ず(中途半端な、しかも実際は中点ではない)整数として計算されて、収束計算が終わらず無限ループに突入してしまうのです。それを避ける為には、割る方の数、この場合は2ですが、これを実数型として認識させる為にあえて小数点以下を記述して2.0としなければならないのです(もちろん、与える引数を必ず実数にする、って回避法もあるんですが、メンド臭いでしょう)。
この辺は一回はハマるので気をつけて下さい(僕も良くハマります・笑)。
Rubyのコードでの注意点その2。引数に関数fを取る、と言う形にしていますが、コード内部でその関数fを引数aとbや変数mに適用しています。こう言う場合、Rubyでは特殊な書法を採用していて、そう言う場合、f[a]とかf[b]とかf[m]と記述します。決して数学記法と同様にf(a)とかf(b)とかf(m)とか記述しないように。括弧が違うのです。
Rubyでのコードでの注意点その3。Rubyでは絶対値を取るとき特殊な記法を用います。通常、絶対値を取る場合、絶対値を取る関数abs(大体のケースでは、絶対値=absolute valueからこの名前が付いている)は絶対値を取りたい数値、ないしは式の(どんな形であれ)前方に書くんですが、Rubyの場合は数値/式.absと言う書き方をします。これはかなり変わった書法に見えますが、こう言うのは実はオブジェクト指向型言語と呼ばれる言語では割に良く見かける書き方です。Rubyはオブジェクト指向型言語だ、と言うのをどっか頭の片隅にでも置いておいてください(そして、それはそれでかなり一環しているのがRubyの特徴です)。

Schemeによる二分法





(define (BisectionMethod f a b)
(let ((eps 1.0E-06)
(m (/ (+ a b) 2))
)
(cond ((positive? (* (f a) (f b)))
(error a b))
((< (abs (- a b)) eps)
(exact->inexact a)) ;ここに注意
((positive? (* (f a) (f m)))
(BisectionMethod f m b)) ;ここで再帰
(else
(BisectionMethod f a m)) ;ここで再帰
)))

再帰部分はまあ、良いですよね。
Schemeのコードでの注意点は、まあ、注意点、って程ではないんですけど、最後の結果を返す場合、単にaを返すのではなくってexact->inexactと言う命令を使って浮動小数表記に変換した方が見やすい、と言う事です。
と言うのも、Schemeはプログラミング言語としては珍しく分数を扱えます。これは元々、世界の数理科学系大学の頂点と言うべきマサチューセッツ工科大学が「工学上の限界で不正確な値を返すプログラミング言語を好まなかった」と言う事に由来しています。それじゃあ数理科学の授業で(あくまで)数学的理論を説明するのが困難になるから、です。
この事を解決する為に、マサチューセッツ工科大学は「分数で返せる計算は分数で返す」と言うとてつもない暴挙に打って出て、従って、少なくとも表面的には、Schemeの二分法では「分数を使って延々と繰り返し計算を行っている」(!)のです。これは他のプログラミング言語ではあまり見られない(しかも優秀な)特徴です。
従って、単純にaを返させると分数表記で返してくる、んですね、Schemeは。そこでexact->inexactと言う命令で(意味は正確数から不正確数への変換を表す)一般的な「望まれる」結果に変換しておくわけです。

Rによる二分法





BisectionMethod <- function(f, a, b) {

eps = 1.0E-06
m = (a + b) /2

if (f(a) * f(b) > 0) {
return(NA)
}
else if (abs(a - b) < eps) {
return(a)
}
else if (f(a) * f(m) > 0) {
return(BisectionMethod(f, m, b)) #ここで再帰
}
else {
return(BisectionMethod(f, a, m)) #ここで再帰
}
}


再帰部分はまあ、良いですよね。
この中では一番、特に問題が無く「思った通りの」計算結果を返してくれるのがRです。さすが統計解析(数値計算)専門のプログラミング言語だけあって良く出来ています。

無名関数


さて、もう一度繰り返しますが、通大の「コンピュータ」テキストのp.144の例示は、「$f(x)=x^2 sin(x)$と言う関数"だけを"2分法で計算する」プログラムです。万が一、$f(x)=x^2 sin(x)$「以外の」関数に2分法を適用したい場合、一々プログラム本体に埋め込まれたx*x*sin(x)を随時修正して「コンパイルしなおして」結果を見ないとなりません。
これは、あまりにもメンド臭いんで、ここでは「任意の関数fも引数として与えて」計算させるスタイルを取りました。
ここでは、その「引数としての関数fの与え方」を説明しようと思います。これは各言語毎に違います。

その話をする前に、通大テキストで扱われていた$f(x)=x^2 sin(x)$のグラフをちょっと見てみましょうか。



こう言う場合もMaximaは便利ですね。チャッチャとグラフが描けます。また、作った数値計算のプログラムの動作確認をする場合にも、こう言った「既存の」アプリケーションは重宝します。計算結果を照らし合わせる事が出来る。
さて、任意の点a、bを選びましょうか。2分法の論理によると、

  1. 収束値の中点mはf(x)とx軸が交差している点である。

  2. f(a)とf(b)は異符号でなければならない。


との事でした。動作確認をする為に逆に考えていくわけですね。
そうすると、グラフを見ると、例えばx=3付近でf(x)はx軸との交点を持っています。そしてその両側のある範囲はどう見ても異符号ですよね。
そこで。キリの良い数字として、a=2、b=4を初期値としましょう。
次に、各プログラムに任意の関数fを与えるんですが、プログラミング言語毎に違う記法があります。
ざーっと紹介しますが、


  • Pythonの場合:lambda 変数: 式

  • Rubyの場合:lambda{|変数| 式}

  • Schemeの場合:(lambda (変数) (式))

  • Rの場合:function(変数){式}



と言う形で記述します。これらの記法は「名前の無い関数」と言う意味で、通称無名関数と呼びます(あるいは、R以外はすべてlambdaと書いているのでラムダ式、と言う言い方もします)。
例えば、今は$f(x)=x^2 sin(x)$が対象となる関数なんで、それぞれの言語に合わせた無名関数は


  • Pythonの場合:lambda x: x * x * math.sin(x)

  • Rubyの場合:lambda{|x| x * x * Math.sin(x)}

  • Schemeの場合:(lambda (x) (* x x (sin x)))

  • Rの場合:function(x){x * x * sin(x)}



となります。形式的には$f(x)=x^2 sin(x)$として与えている、と言う事が分かるでしょう。
これで、任意の関数fを引数として与える事が出来るのです。
従って、それぞれのインタプリタ上での入力コマンドは、


  • Pythonの場合:BisectionMethod(lambda x: x * x * math.sin(x), 2, 4)

  • Rubyの場合:BisectionMethod(lambda{|x| x * x * Math.sin(x)}, 2, 4)

  • Schemeの場合:(BisectionMethod (lambda (x) (* x x (sin x))) 2 4)

  • Rの場合:BisectionMethod(function(x){x * x * sin(x)}, 2, 4)



となります(注:Pythonだけは、mathモジュールを明示的に呼び出す必要があるので、インタプリタで上の関数を走らせる前にimport mathと言うコマンドを走らせて下さい)。
恐らくどれも結果は、桁数は違いますが、


3.1415920257568359


辺りの値を返してくる、と思います。これは$sin(\pi)=0$から、$f(\pi)=\pi^2 sin(\pi)=0$になるのは容易に想像付きますね。だから円周率π回りの値を返してきたのです。
また、円周率 1,000,000 桁と合わせてみても少数点以下第6桁までは数値があってるのが分かるでしょうか?これは計算誤差εの精度を$10^{-6}$にした事に由来しています。これ以降は「誤差を含む」結果を返してる、と言う事ですね。

これでこれら2分法のプログラムは「上手く動いている」のが分かりました(と言うか、数学的に見て「上手く動く」のは当たり前なんですが・笑)。
他にも様々な無名関数や初期値a、bを与えてみて動作を確認してみて下さい。

ちなみに、質問<3331>なんかはこれがベースの課題ですね。


[課題3・2]  関数f(・)とその導関数f'(・)が与えられているとき、方程式f(・)=0の解をニュートン・ラフソン法により求めよ。

ニュートン・ラフソン法は理論的には2分法と比べて難しいのですがプログラムは逆に作り易くなります。基本部分は前のプログラムと同じですから一気にプログラムを書き上げてしまいましょう。


  • プログラム  「ニュートン・ラフソン法」

    • 定数 ε=1.0E-06 : 実数型

    • 変数 x0、x : 実数型

    • {
    • x0の値を読み込む

    • x←x0−f(x0)/f'(x0)

    • |x−x0|≧εである限り次を繰り返す
      {

      • x0←x

      • x←x0−f(x0)/f'(x0)


      }
    • xの値を出力する


    • }




ここで、変数x0は前の反復より得られた値を保存しておくためのものです。その値を用いてさらに計算をし、結果を変数xに格納します。xとx0の値が十分近ければ、それは方程式の近似解であるとみなして反復を終了します。
定理の仮定は比較的狭い区間で満たされることが多く、初期値の与え方によってニュートン・ラフソン法で目的の解に収束しないときがあります。実際に使う場合は、2分法を併用したり、おおよそのグラフを書く等の注意が必要です。

<演習問題>

  1. 手元の電卓で(1÷3)×3を実行し、結果を確認せよ。

  2. ニュートン法は初期値によって目的の解が得られないときがある。その解をグラフで考えよ。




ええと、ニュートン・ラフソン法ですか。確かに大して難しく見えません。
ところで、ここでのニュートン・ラフソン法は、

関数f(x)の導関数f'(x)が分かっている場合

との事なんで、微分自体をプログラムで行うわけではないと言うトコがミソです。
例えば、通大「コンピュータ」テキストのp.145のPascalでのサンプルプログラム「NewtonRaphson」を見てほしいんですが、ここにもロジックに全く関係がないf:=x*x - 2の記述やらDf:=2*xの記述やらがありますね。2分法の時でも見たように、ここのプログラムも$f(x)=x^2−2$専用のニュートン・ラフソン法のプログラムになってると言う事です。これも$f\prime(x)=2x$なんで、ちょっと見れば分かるでしょう。
従って、微分自体は人手で行わなければダメだ、と言う事です。
もっとも、プログラムでの自動微分は一般に難しい為、これはこれで妥当な戦略だ、とは言えます。
(ここでもMaximaなんかはかなり自由自在に微分/積分を行ってくれているので、如何に優秀なソフトウェアなのか分かるでしょう。数値計算とはまた違う記号処理と言う方法を用いているのですが、一般的にプログラミングはとても難しいです。)
そこで、ここではf(x)と微分係数Df(x)の二つも(無名関数での)引数として与える、と言う事にしてみます。シナリオは以下の通りです。


  1. ここでは取り合えず、f、Df、x0と言う三つの引数を取るプログラムをNR(f, Df, x0)と呼ぶ。

  2. 計算誤差εを$$\epsilon=10^{-6}$$とする。

  3. x=x0−f(x0)/f'(x0)とする。

  4. もし、|x−x0|<εなら結果としてxを返す。

  5. そうじゃなかったらNR(f, Df, x)を呼び出す。←再帰!!!



本質的には局所変数x=x0−f(x0)/f'(x0)を設定する必要は無いんですが、プログラム内にxを呼び出すトコが多いんで、こう言う形になっています。要するに一々右辺のx0−f(x0)/f'(x0)を書くのがメンド臭い、と言う建設的な理由に基づいています(笑)。こう言う感じで「良く使う計算式は局所変数にしてしまう」と言うのはアリです。その辺は各自色々工夫してみて下さい。
では実装に入りましょうか。

Pythonによるニュートン・ラフソン法





def NewtonRaphson(f, Df, x0):

eps = 1.0E-06
x = x0 - 1.0 * f(x0)/Df(x0) # ここに注意

if abs(x - x0) < eps:
return x
else:
return NewtonRaphson(f, Df, x) # ここで再帰




再帰はまあ、良いですね(いい加減慣れてきたでしょう・笑)。
Pythonの注意点はx = x0 - 1.0 * f(x0)/Df(x0) の部分なんですが、決してx = x0 - f(x0)/Df(x0)とは書かない、と言う事です。疑似コードには無い1.0 *を付け忘れちゃいけない。
理由は2分法の場合と同じです。Pythonは整数/整数の計算だと整数を返しちゃうから、なのです。 1.0 *を付け足す事によって、結果1.0 * f(x0)は整数ではなくって実数だと認識され、以降の収束計算が滞り無く行われる、と言うわけです(逆に言うと1.0 *が無いと収束計算が上手く行きません)。
亀田自身はこれは結構不具合なんじゃないか?とか思います。実際、Pythonの提供元もこれは不具合だ、と認めているようで、将来的にリリースされるPython3.0では改良予定だそうです。少なくとも、「動的型付け言語」で数値の型を色々気にしながらプログラムを打つのはあまり効率的ではない、と僕も思います(しかも明示的に型変換する方法も見当たりませんしね)。

2009年1月30日追記:新しくPython3.0がリリースされている模様です。亀田はまだ確かめていません。
従って、ここで提示されているコード類は既に少々古くなっています。亀田が使用したPythonのヴァージョンはPython 2.5.2です。いまだ配布されている現役のヴァージョンなんで、取り合えずPython2.5.2を使っていて下さい。Python3.0用に記事を書き換えるかどうか、は今のところ未定です。


Rubyによるニュートン・ラフソン法





def NewtonRaphson f, df, x0 # ここに注意1

eps = 1.0E-06
x = x0 - f[x0].to_f / df[x0] # ここに注意2

if (x - x0).abs < eps
x
else
NewtonRaphson(f, df, x) # ここで再帰
end
end




再帰はまあ、良いですね(いい加減慣れてきたでしょう・笑)。
Rubyの注意点その1。疑似コードでは微分係数での引数をDfと表記してますが、Rubyでは引数に大文字は使えないようです。従って、Ruby版では微分係数を表す引数をすべてdfにしてあります。
Rubyの注意点その2。x = x0 - f[x0].to_f/df[x0] の部分なんですが、決してx = x0 - f[x0]/df[x0]とは書かない、と言う事です。疑似コードには無い.to_fを付け忘れちゃいけない。
理由は2分法の場合と同じです。Rubyは整数/整数の計算だと整数を返しちゃうから、なのです。 .to_fを付け足す事によって、明示的にf(x0).to_fは整数ではなくって実数に変換され、以降の収束計算が滞り無く行われる、と言うわけです(逆に言うと.to_fが無いと収束計算が上手く行きません)。
Rubyはこのような明示的な型変換のメソッドを持っています。また、この記述法にも一般的なオブジェクト指向言語の影響が見られます
ちなみに、.to_fは"to float"、つまり「浮動小数点数(実数)に変換せよ」と言う命令を意味しています。

Schemeによるニュートン・ラフソン法





(define (NewtonRaphson f Df x0)
(let ((eps 1.0E-06)
(x (- x0 (/ (f x0) (Df x0))))
)
(if (< (abs (- x x0)) eps)
(exact->inexact x)
(NewtonRaphson f Df x)) ;ここで再帰
))


これは問題無いでしょう。再帰も既に慣れてきたでしょうしね。

Rによるニュートン・ラフソン法





NewtonRaphson <- function(f, Df, x0) {

eps = 1.0E-06
x = x0 - f(x0) / Df(x0)

if (abs(x - x0) < eps) {
return(x)
}
else {
return(NewtonRaphson(f, Df, x)) #ここで再帰
}
}




これも特に問題無いでしょう。再帰も既に慣れてきたでしょうしね。
なお、Rは統計解析/数値計算ソフトなので、ニュートン法を実行するunirootと言う組み込み関数がある事を申し添えておきます。

さて、$x^2-2$の解を考えると$x=\sqrt{2}$、$x=-\sqrt{2}$になる事は分かります。これは非常に確かめやすいですね。
無名関数を利用して、初期値x0を2として各プログラムで解を一つ求めてみましょう。インタプリタに入力するコマンドは以下の通りです。

  • Pythonの場合:NewtonRaphson(lambda x: x * x - 2, lambda x: 2 * x, 2)

  • Rubyの場合:NewtonRaphson(lambda{|x| x * x - 2}, lambda{|x| 2 * x}, 2)

  • Schemeの場合;(NewtonRaphson (lambda (x) (- (* x x) 2)) (lambda (x) (* 2 x)) 2)

  • Rの場合:NewtonRaphson(function(x){x * x - 2}, function(x){2 * x}, 2)


これで計算すると、恐らく初期値2の近傍の解として、桁数は違うでしょうが、次のような数値が得られる事と思います。

1.4142135623746899

これは確かに$\sqrt{2}$に極めて近い値となっています。
また、これも一応、確実な値は小数点以下第6位まで、となっています。それ以降は計算誤差を含んでいるんで、あまり信頼出来ませんね。
他にも関数fと初期値x0を色々と変えてみてニュートン・ラフソン法を試してみて下さい。

<演習問題回答例>

  1. 省略。

  2. これはこのページを参考の事。

最大公約数と最小公倍数

さて、最大公約数と最小公倍数、です。
実はこれらの計算アルゴリズムもLisp系では「再帰の例」として良く使われる例、です。
ほらね、このセンセ、やっぱ「Lispっぽいネタ」が大好きなのです(多分、東工大の情報工学系の学生は皆やらされます・笑)。
しかし、これらの「再帰だったら簡単に書ける計算アルゴリズム」を「反復」で書かなければならないとしたらどうなるか?
これは非常に大変、です。そして説明が大変になる。従って読んでる読者はもっと大変になる、って事です。
以下はその典型例だ、って事を含んで始めましょうか。


[課題2・1]  2つの正の整数が与えられたとき、その最大公約数を求めよ。

プログラム作成の第1段階として上の問題を次のように分解しましょう。

  • 2つの正整数m、nを入力する

  • mとnの最大公約数GCDを求める

  • 最大公約数を出力する


1番目の入力部分については、非正値の入力があればプログラムを強制終了することにし、次のように分解します。

  • 正整数m、nを読み込む

  • mとnのどちらか一方が非正値であればプログラムを強制終了する


2番目の命令について、後からその手順を詳細に考えることにし、いまはそれを実行してくれる手続き"最大公約数(m、n、GCD)"があるものとします。この手続きは、2つの正整数m、nを与えると、その最大公約数GCDを返すものとします。
最後の出力は、前節と同様にこれ以上詳細に考えません。
これから、メインプログラムは次のように書かれます。

  • プログラム  「最大公約数を求める」

    • 変数 m, n, GCD : 整数型


    {

    • m、nを入力する

    • もし  (m≦0またはn≦0)
          ならば プログラムを終了

    • 最大公約数(m, n, GCD)

    • GCDの値を出力する


    }






ここから、最大公約数を求める具体的な手順を考えていきましょう。数学的には、最大公約数を求める方法はいくつかあります。ここではユークリッドの互除法によって最大公約数を求めてみます。
ユークリッドの互除法が拠り所とするのが、次の定理です。

[定理]  2つの正の整数m、n (m≧n) があるとき、mをnで割った余りをr (0≦r<n)とすれば、

  1. r = 0ならば、mとnの最大公約数はnである

  2. r ≠ 0ならば、mとnの最大公約数は、nとrの最大公約数に等しい


上の1.はあきらかですし、2.も簡単ですからこの証明は省略します。演習問題として自分で考えてください。

下線部:亀田


はいはいはいはいはい、ストップストップストップ。
ここで、世界最古のアルゴリズム、と呼ばれる「ユークリッドの互除法」が表れました。そして、実はこの「ユークリッドの互除法」自体が数学的に言えば「帰納」、プログラミング用語で言うと「再帰」の構造である、ってのが分かるでしょうか?
ここで、mとnの最大公約数を取り合えずGCD(m、n)と置いてみて、また、mとnの剰余をm mod nと書き表す事とします。
そうすると、下線部の部分に注視すると、次の「たった」3ステップで「ユークリッドの互除法」を表現出来るんです。


  1. 正整数m、nを読み込む。mとnのどちらか一方が非正値であればエラーを返す

  2. m mod n = 0ならば、GCD(m, n)はnである

  3. m mod n ≠ 0ならば、CCD(m, n)は、GCD(n, m mod n)である



ここまで分かれば早速実装です。んで、不思議に思うかもしれませんが「これで上手く動く」んですよ(笑)。



# -*- coding: utf-8 -*-
# Pythonの場合

def GreatestCommonDivisor(m, n):
if m <= 0 or n <= 0:
raise ValueError
elif m % n == 0:
return n
else:
return GreatestCommonDivisor(n, m % n) # ここで再帰

# Rubyの場合

def GreatestCommonDivisor m, n
if m <= 0 || n <= 0
raise
elsif m % n == 0
n
else
GreatestCommonDivisor(n, m % n) # ここで再帰
end
end

;; Schemeの場合

(define (GreatestCommonDivisor m n)
(cond ((or (<= m 0) (<= n 0))
(error m n))
((zero? (modulo m n))
n)
(else
(GreatestCommonDivisor n (modulo m n))) ;ここで再帰
))

## Rの場合

GreatestCommonDivisor <- function(m, n) {
if (m <= 0 || n <= 0) {
return(NA)
}
else if (m %% n == 0) {
return(n)
}
else {
return(GreatestCommonDivisor(n, m %% n)) #ここで再帰
}
}


不思議でしょ(笑)?単に「数学的定義」を「そのまま」置き換えれば良いのです。「反復」なんて影も形も出てきませんし、「それでも動く」のは、元々数学自体が「そう言う風に」作られている、って事なんです。一旦枠組みさえ大まかに設定してしまえば、「個々の細かい計算」は全然考えなくって良い。あとはコンピュータ(と言うか、プログラミング言語)任せで良い、んです。
そろそろ「再帰」と言うプログラミングに於ける数学的手法が如何に強力なのか、分かってきたでしょ(笑)?「再帰」が使えないプログラミング言語なんて学ぶに値しない、って言った意味を。
ちなみに、「再帰が難しい」とか思うんだったら、数学が分からん、って意味です。数列や漸化式、数学的帰納法が全くわかっていない、って事なんで、「数学教員免許取得」なんて諦めた方が良い、です。あるいは、一旦プログラミングの学習を中止して、通大のテキストで良いんで、解析をもう一度やり直してください。その理論は数学の範疇なんで、プログラムをやる前に数学で押えておくべきネタです。
いずれにせよ、これを「再帰」ではなく「反復」で実装するのはなかなか骨が折れる仕事です。まあ、練習代わりにはやっても良いでしょうが、基本的にコードは「長く」なりがちですね。
以下の通大テキストの抜粋は「反復」を使う場合のコードの書き方、になります。


この命題から、2つの正整数m、nがあるとし、わり算を続けて行い

$$n > r_1 > r_2 > r_3 > r_4 > \ldots \ge 0$$

という整数で各わり算の余りを実現したとき、


  • r1 = mをnで割った余り

  • r2 = nをr1で割った余り

  • r3 = r1をr2で割った余り

  • r4 = r2をr3で割った余り

  • ・・・

  • ・・・



であるとすれば、この演算の繰り返しは必ず有限回で終了します。最後のわり算は割り切れて

  • 0=rkをrk+1で割った余り


となります。このときrk+1が、mとnの最大公約数となります。これが、ユークリッドの互除法と呼ばれる方法です。ここで、定理の仮定にあるようにm≧nを確かめていませんが、m<nのときは、

  • r1=mをnで割った余り=m

  • r2=nをr1で割った余り=nをmで割った余り

と考えられますから、手順としてその確認は必要ありません。この数学的手順を、これからプログラムとして書かなくてはなりません。
数学的表現では添え字を使って、すべての余りを区別しています。これにより、なにを数学的にやっているのか分かりやすい表現となります。では、プログラム上も配列等を使って、それらを区別して記憶しなくてはならないのかというと、その必要はありません。ここでは、最大公約数として最後のrk+1だけが必要です。したがって、1つの変数rを用意して、新たな余りが得られるたびにその値を変数rに代入すればよいことになります。これは、数学では変数はなにか具体的な値そのものを表しており、時間的に変化しません。そこで、添え字を使って、別の変数として記述することによりその変化を表現します。しかし、コンピュータプログラムにおいては、変数の中身は時間(処理が進むにつれてという意味です)によってダイナミックに変化していきます。
ユークリッド互除法による最大公約数を求めるプログラムを手続きの形で書いてみましょう。


  • 手続き 最大公約数(m:整数, n:整数, GCD:整数型変数)

    • 局所変数  d, r, t : 整数型


    {

    • d←m

    • r←n

    • (r≠0)であるかぎり次を繰り返す
      {

      • t←dをrで割った余り

      • d←r

      • r←t


      }

    • GCD←d


    }



この手続きが呼び出されると、仮引き数mとnの最大公約数が変数GCDの中に格納されます。mとnは、値だけをサブプログラムに渡す引数ですが、GCDは呼び出し先に値を返さなければならないため変数の仮引数となります。

局所変数dとrは、1つ前に実行されたわり算における除数divisorと剰余residueの値を格納するための変数です。

  • n=○○をmで割った余り


と解釈すれば、このわり算において、除数はmで余りはnとなります。そこで、dにmを代入、rにnを代入します。ユークリッドの互除法は、余りが0になるまで1つ前に実行されたわり算における除数を被除数に、また、余りを除数にした新たなわり算をおこない余りを求めていきます。それが

  • l←dをrで割った余り


の部分です。ここでは、余りをすぐにrに代入せずに一時的に変数tに代入しています。ここで実行されたわり算の除数であるrの値を、後のためにdとして保存しておかなければならないからです。

  • r←dをrで割った余り


と、すぐに余りを求めてそれをrに代入したのでは、次のわり算に必要な代入前のrの値が失われてしまいます。プログラムを書くとき、このように一時変数に値を保存しておき、後で必要となる値を失わないようにします。代入の順序に注意してください。
先ほどのメインプログラムの後に、この手続きを書けば課題に対するプログラムとなります。






極めて長ったらしい説明ですよね(笑)。さすがに読んでる途中で嫌になってきます。
もう一度繰り返しますが、元々「ユークリッドの互除法」と言うアルゴリズム自体が「再帰」的、なんです。従って、再帰的アルゴリズムを「反復」に直す事は極めて「不自然」で、上のように「長ったらしく」考えないと「反復」に変換出来ない、って事を意味しています。
91年当時のパソコンの「テクノロジー」ってのは、こう言う風に「再帰を楽々扱えない」限界が存在してた、って言い方も出来るのです。


[課題2・2]  2つの正の整数が与えられたとき、その最小公倍数をもとめよ。

この課題のメインプログラムは、前の課題のプログラムにおける最大公約数を求める手続きを最小公倍数を求める手続きに変えるだけです。
2つの正整数mとnが与えられたとして、その最小公倍数を求める手順を考えてみましょう。このための数学的手順もいくつかありますが、上で作った最大公約数を求めるプログラムを利用することを考えてみます。

m×n=(mとnの最小公倍数)×(mとnの最小公約数)

が成立することは、よく知られた事実です。これから、すでに作成してある最大公約数を求める手続きを使って、最小公倍数を求める手続きは次のようになります。

  • 手続き 最小公倍数(m : 整数, n : 整数, LCM : 整数型変数)

    • 局所変数 GCD整数型


    {

    • 最大公約数(m, n, GCD)

    • LCM←(m×n)/GCD


    }



これから、課題のプログラムを完成させることは容易ですから省略します。

<演習問題>

  1. ユークリッドの互除法の拠り所となる定理を示せ。

  2. 3つの正整数a、b、cの最大公約数と最小公倍数を求めるプログラムを作成せよ。




まずは課題2・2を解いてしまいましょう。
基本的には全然難しくないんですが、ある「流儀」を説明しておこうと思います。
それは別ファイルに保存した「プログラム」を呼び出したりする方法です。

課題2・2自体は全然難しくないんですが、このプログラムでは先に作ったプログラム「GreatestCommonDivisor」を呼び出さなければなりません。そしてその「呼び出し方法」にはプログラミング言語毎に流儀があります。
まずは、プログラムを書いたテキストファイルを保存するには「拡張子」と言うものを保存するテキストファイルの名前に付けないとなりません。一般にOS(オペレーティングシステム)はこの「拡張子」によって、そのテキストファイルが何のプログラムなのか識別するようになっているから、です。

  • Pythonプログラムの拡張子は".py"である。

  • Rubyプログラムの拡張子は".rb"である。

  • Schemeプログラムの拡張子は".scm"である。

  • Rプログラムの拡張子は".R"である。


ここでは、先ほど作ったGreatestCommonDivisorを記述したテキストファイルをそれぞれ

  • Pythonの場合は"GCD.py"として保存。

  • Rubyの場合は"GCD.rb"として保存。

  • Schemeの場合は"GCD.scm"として保存。

  • Rの場合は"GCD.R"として保存。


したとして、それぞれのコードがどうなるか見てみます。
なお、プログラムの保存場所はどこにすべきか?と言う問題がありますが、一般に、デフォルト指定されている場所(フォルダ/ディレクトリ)に保存すれば大丈夫、です。その辺にはデフォルトでパスが通ってるでしょうから。



# -*- coding: utf-8 -*-
# Pythonの場合

import GCD # これが呼び出し方

def LeastCommonMultiple(m, n):
return (m * n) / GCD.GreatestCommonDivisor(m, n)
# Pythonではドット(.)は〜の、と言う意味

# Rubyの場合

load "GCD.rb" # "GCD.rb"をロード

def LeastCommonMultiple m, n
(m * n) / GreatestCommonDivisor(m, n)
end

;; Schemeの場合

(load "GCD.scm") ;"GCD.scm"をロード

(define (LeastCommonMultiple m n)
(/ (* m n) (GreatestCommonDivisor m n))
)

## Rの場合

source("GCD.R") #これが呼び出し方

LeastCommonMultiple <- function(m, n) {
return((m * n) / GreatestCommonDivisor(m, n))
}


<演習問題解答例>

  1. Phaos氏が数学的に説明してるんで、それをパクります(笑)。

  2. まずは作っておいたLeastCommonMultipleと言う関数(あるいは手続き/メソッド)を

    • Pythonの場合は"LCM.py"として保存

    • Rubyの場合は"LCM.rb"として保存

    • Schemeの場合は"LCM.scm"として保存

    • Rの場合は"LCM.scm"として保存


    します。こうやって保存した"GCM.*"と"LCM.*"を再利用して演習問題を解きます(これが、通大のテキストに書かれてある「分割」「統合」の例だ、と言う事を忘れないように!!!)。
    なお、これは問題としては単純で、一般に3変数a、b、cの最大公約数、最小公倍数を求める場合、最大公約数のアルゴリズムをGCM、最小公倍数のアルゴリズムをLCMとすると、

    • 3変数a、b、cの最大公約数=GCM(a, GCM(b, c))

    • 3変数a、b、cの最小公倍数=LCM(a, GCM(b, c))


    となります。こう言う表記を入れ子(あるいはネスト)と呼ぶのです。
    つまり、上の「入れ子表現」をそのまま書けば解答としては一丁上がり、って事なんです。
    なお、通大のテキストだと一々出力命令を噛ましていますが、一般に、出力命令を噛ますと再利用がしにくい、と言う欠点が生じます(これはC言語でも同様です)。
    従って、ここでは2つの計算結果を返す為、リスト(配列)を用いて計算結果を返すようにしてみました。


    # -*- coding: utf-8 -*-
    # Pythonの場合

    import GCD, LCM # 二つのファイルをインポート

    def GCDandLCM(a, b, c):
    return [GCD.GreatestCommonDivisor(a, GCD.GreatestCommonDivisor(b, c)),
    LCM.LeastCommonMultiple(a, LCM.LeastCommonMultiple(b, c))]

    # Rubyの場合

    load "GCD.rb" # "GCD.rb"をロード
    load "LCM.rb" # "LCM.rb"をロード

    def GCDandLCM a, b, c
    [GreatestCommonDivisor(a, GreatestCommonDivisor(b, c)),
    LeastCommonMultiple(a, LeastCommonMultiple(b, c))]
    end

    ;; Schemeの場合

    (load "GCD.scm") ;"GCD.scm"をロード
    (load "LCM.scm") ;"LCM.scm"をロード

    (define (GCDandLCM a b c)
    (list
    (GreatestCommonDivisor a (GreatestCommonDivisor b c))
    (LeastCommonMultiple a (LeastCommonMultiple b c))
    ))

    ## Rの場合

    source("GCD.R") #"GCD.R"をロード
    source("LCM.R") #"LCM.R"をロード

    GCDandLCM <- function(a, b, c) {
    return(list(GreatestCommonDivisor(a, GreatestCommonDivisor(b, c)),
    LeastCommonMultiple(a, LeastCommonMultiple(b, c))))
    }

    はい、問題無いですね。
    以上です。

総和


[課題1・1] 1からnまでの和、$\sum_{k=1}^n k$を求めよ。

最初の議題として、1からnまでの和を求めるプログラムを作成することにします。この問題をコンピュータにやらせる場合、第1段階として、問題を次のようにおおまかに分解します。このとき、直接コンピュータが理解できるかできないかは考えません。

  • 正の整数nを入力する

  • 1+2+・・・+nを計算する

  • 上の計算結果を出力する


最後の出力命令は、コンピュータの基本命令の中に存在します。これ以上、ここでは詳細化しないことにします。ただし、その形式は人間のように融通がきくものではないことに注意してください。望むような形式で出力を行いたいときは、やはり、コンピュータにそのための手順を詳細に指示しなくてはなりません。

1番目の命令は、一見これ以上細かくすることができないようにみえますが次のように細分化しなければなりません。入力したnの値が正の整数である保証はありません。

  • nを入力する

  • nの値が正か調べる


この順序は変えられません。nの値が入力されない限りは、正負の判定ができないからです。次に、nの値が正でないとき、どうすればよいかも考えなくてはなりません。ここでは、強制的にプログラムの実行を終了することにします。これは本来の目的とはあまり関係ないことのようですが重要な問題です。プログラムの間違いをなくし信頼性を増すためにも、「nに負の値が入力されるはずはない」というような、思い込みを持ってはいけません

下線部:亀田


うわあ、これ全部を「プログラムを書く前に」考えるんなら、確かにプログラム書くのは億劫でしょう(笑)。すげえな(苦笑)。これ読んでたらさすがに「大仕事」に感じて嫌気が差してきます(笑)。
いや、間違っちゃいないでしょう。確かに言ってる事は正しい、です。
でも、僕だったら「いきなり書き始め」ますね。何も考えません(笑)。
僕みたいに「いきなりコードを書き始める」スタイルを、ラピッド・プロトタイピング(rapid prototyping)と呼びます。そして、今はそっちの方が「流行り」なのです。
ラピッドプロトタイピング、と言うのは、「速い試作品化」って意味です。元々UNIXなんかでも

できるだけ早く試作を作成する

Mike Gancarz

のがベストだと言われています。
ちょっと長いですが、UNIXという考え方―その設計思想と哲学と言う書籍から引用してみましょうか。

「できるだけ早く」とは、本当に「できるだけ早く」ということだ。「大至急」だ。アプリケーションの立案に少しの時間をかけ、あとはまっしぐらに進むだけだ。命をかけてコードを書く。端末が熱いうちにキーボードを打つ。1ナノ秒も無駄にする時間はない。
この考えは、多くの人が考える「正しい設計作法」と真っ向から対立している。ほとんどの人は「アプリケーション設計を完全にしてからコーディングにかかれ」と教えられている。「まず、機能仕様書を書かなくちゃ」と言う。定期的なミーティングで方向性を確認する。細部まではっきりさせた考えを設計仕様書に書く。90パーセントの設計ができて始めてコンパイラを使う。
原理的には、もっともな指針に聞こえる。しかしこの考え方は、試作というものの重要性を見過ごしている。あるアイディアがものになりそうかどうか、目に見える現実的な場面でテストするには試作が一番だ。試作以前のアイディアは、これはこう動くはずだという憶測の域を出ない。この時点では、設計構想はほとんど理解されておらず、おそらく人によって解釈が異なっているだろう。プロジェクトの前進には、全員の合意が必要だ。試作は、具体的に目標を示すことで全員の合意を醸成する。

ほら、全然真逆の事書いてるでしょう(笑)。
実は、UNIXなんかは「そんなに窮屈な」システムでも無いんです。もちろんその延長線上で言うと、C言語さえ「そんなに窮屈な」言語でもありません(もちろん難しいですが)。
大事なのは「やっつけ仕事」なんです。そして、UNIXにはその「やっつけ仕事用」のツールが山ほど揃ってるのです。
さて、「いきなりプログラムを書き始められる」のは僕が頭が良いから、でしょうか?あるいは慣れてるから?どっちも違いますね。単に「やっつけ仕事で早く目的に達成できる」ツールをプログラミング言語として選んでいるから、に過ぎません。
もう、PythonやRubyを弄ってみた人なら分かるでしょうが、通大御用達の「疑似プログラム」で「紙の上に」プログラムの流れ、なんて書くよりもPythonやRubyで「いきなり」書いちゃった方が速い、ってのは何となく分かったでしょう。それが下手な「プログラム設計書」より良い、と言う事が。「紙の上に」疑似コード書いていったって「実際に動く」わけではありません。反面、PythonやRubyなら「紙の上に」疑似コードを書く程度の労力で、しかもコッチの方は「目の前で動く」んです。どうしてPythonやRubyが「動く疑似コード」と呼ばれるのか、そろそろ分かってきたんじゃないか、と思います。
そして、そう言う言語の方が、今は人気があるのです。ラピッド・プロトタイピングが「21世紀型の」プログラミング法、なのです。
もちろん、システム・プログラミング(例えばOSとかのプログラミング)は同様な文脈では語れませんが、数値計算のプログラムを書く程度で……余計な脅しは必要無いですよね。


本来の目的とはあまり関係ない


ホント関係無い、ですよ(笑)。関係ない事は書くべきじゃないです。脅しが好きだなあ(苦笑)。
なお、一応ここでは「プログラムの実行を終了する」のではなくってエラーを返す方向で対処していきましょうか。


プログラムの間違いをなくし信頼性を増すためにも、「nに負の値が入力されるはずはない」というような、思い込みを持ってはいけません


そうか(笑)?まあ、そう言う部分もあるでしょう。
ただし、です。プログラマ側が「全ての入力値を予測出来る」ってわけでも無いでしょう。だったらいきなり「文字」を入力されたら?どうすんだろ(笑)?
このテの問題は、ラピッド・プログラミングの文脈では「問題が起きてから修正する」方が結果効率的、なんです。大体、プログラミングの世界では「ユーザーはいつもプログラマが予測もしないソフトウェアの使い方をする」と言う格言がある程、なんですよ。全ての「禁じ手」を列挙しよう、ってのは土台無理なんです。
大体、ここでやってんのは「個人で使う」数値計算プログラムでしょ?しかも学科が「数学コース」ですし。何かの「商用製品」作ろう、とかじゃないんです。
あんま脅しても良い事ないんじゃないの?とか思うんですけどね。
気楽に行きましょうよ(笑)。「お気楽プログラミング」がここでのモットーです。


2番目の命令をどのようにすれば、コンピュータに理解できるものにすることができるかを考えます。ところで、

1+2+3+・・・+n
=(((((0+1)+2)+3)・・・)+n)

が成り立つことに注意しましょう。この式の右辺は、演算"+"が2項演算であることを認識し、演算の順序まで考えた記述です。コンピュータに計算をさせるときは、この演算の順序が重要な意味をもつことを常に意識しなければなりません。


いやいやいやいやいやいや。ちょっと待て。
確かに

1+2+3+・・・+n
=(((((0+1)+2)+3)・・・)+n)

は正しいですし成り立ちます。んでもそんな考え方普通のプログラミング言語でするか?
ちょっと待て。

しかし、ここでハタ、と気づく。

「これってSchemeだったら正しい解説だよな……。」

実際、Schemeの書き方として以下のやり方はアリです。

(+ (+ (+ (+ (+ 0 1) 2) 3)・・・) n)

確かにそう言う考え方はします。
でもこの奇妙な一致は何故?
もうちょっと先を読んでみましょう。


ここで、変数"和"を用意して、命令

  • 和←和+k


を実行すれば、変数"和"の中の値がkだけ増加します。したがって、最初に変数"和"の値を0にしておき、その値を2増し、3増し、・・・、n増せば、最終的に求める結果が変数"和"の中にあることになります。すなわち、次のように分解された命令を、順番に実行すればよいことになります。

  • 和←0

  • 和←和+1

  • 和←和+2

  • 和←和+3

  • ・・・

  • ・・・

  • 和←和+n


最初に変数"和"に0を代入していることに注意しましょう。変数に最初から0が入っているという保証はありません。
さて、コンピュータは数学等でよく使う省略≦である"・・・"を使って、上のように命令を与えても理解できません。しかし、これは同じことの繰り返しを表現しているのですから、制御構造の1つである反復を使って次のように記述することができます。

  • 和=0

  • kを1からnまでとして次を繰り返す
    {

    • 和←和+k


    }



下線部:亀田


がああああああああああああああああ!!!!!!!
ダメだ、もう我慢が出来ません(笑)。

確かに上の通大用語である「疑似プログラム」の通り書けば良いのは明らか、です。
が、その「繰り返し」をさせる辺りが初心者が躓くトコなんじゃないのか???

ちょっと下線部引いたトコに注目して下さい。
これら抜き書きして見ると手順としては次の3段ステップになってる事が分かるでしょう(0≦k≦nと言う変位幅である事に注意して下さい)。

  1. 入力nがn<0の場合は計算が不可能である。

  2. n=0の時は和=0である。

  3. 和←和+n


もうこうなると、Schemeユーザーとしては次のように書いちゃうのです。一回コード提示しておきますが、上の3ステップとの対応を(Scheme選んだ人以外も)見てみてください。




(define (Summation n)
(cond ((< n 0) (error n)) ;手順1
((zero? n) 0) ;手順2
(else (+ (Summation (- n 1)) n)) ;手順3←ここが重要!!!
))



これで実は[課題1・1]はもう解けちゃってる、んです。「ウソ????」って思うかもしれませんが、本当です。
念のため、もう一回だけ、上のSchemeコードを手順に書き換えてみます。


  1. 入力nがn<0の場合は計算が不可能である。:0以下のnが入力されたらエラーを返す。

  2. n=0の時は和=0である。:nが0だったらそのまま0を返せば良い。

  3. 和←和+n:n番目のSummationはn-1番目のSummationにnを足したものである(ここが重要!!!)。



3番目が大事なのが分かるでしょうか?分かんなかったら上のSchemeコードの4行目を目に穴が開くほど見てください。
これは数学的には次と等価なんです。

$$a_{n} = a_{n-1} + n$$

これをプログラミングの用語で再帰(recursion)と呼びます。そして、数学の用語では帰納(induction)と呼びます(※)。
そして、上のSchemeのコードの一つの特徴は、「分岐」を使っているけど全く「反復」を使っていません。と言うか、「反復は分岐で書くことが出来る」と言うのを指摘した事を覚えているでしょうか?これが「プログラミングに於ける制御の基本2構造で十分だ」と言った最大の理由です。数学では「反復」はいらないのです(が、再帰を用いると自然に反復を表しているのです)。
また、通大のテキストのここまでの解説は、中心的な部分を抜き出してみたら、原理的にこの「再帰」を説明していて、Scheme等のLisp系プログラミング言語での説明だったら「ピッタシカンカン」なんです。
何でこんな事が起こっているのでしょうか?種明かしをしましょう。

亀田が初めてこのテキストを読んだ時、

この説明はLispやSchemeだったら合ってるのにな。

と感じました。
先にも書きましたが、

計算モデルを論じるのだったらLisp系言語が最適

なんです。かつ、明らかにこの数値計算のセクションでは「Lispっぽい説明」が随所に見られます。
学生に「数値計算モデル」を最速で教えるにはLispが最適である。特に薄い、分量が決まったテキストであればあるほど「Lispによる説明」の方が最速で効率的、なんです(だから多くの有名大学の授業ではSchemeが用いられているのです)。

「おっかしいなあ、何でだろ?」

と思ったら、このテキスト後半の筆者、菅原昭博氏、現玉川大学工学部電子工学科准教授のプロフィール見た時、謎が氷解したんです。

「謎は解けたよ、ワトソン君。」

そこにはこんな事が書いてあった。

東京工業大学大学院理工学研究科博士課程修了


「東工大か!!!!!」とか思って(笑)。
いや、東京工業大学、ってのは過去、Lisp専用のコンピュータ、通称「Lispマシン」の開発実績がある程の大学なんです。従って、「東工大」で「情報工学を学んだ」って事は当然Lispを知っています(恐らくSchemeも知ってますよ)。
このセンセ、「Lisp型の解説を」「Pascalでやろう」としてたんですね。道理で随所で歯切れ悪いのも当然だよ、と(笑)。結構、無茶苦茶承知でこのテキスト書いてるんですよ(笑)。
では、だったらどうしてScheme等のLisp系言語でこのテキストを書かなかったのでしょうか?理由は単純ですよね。1990年代初頭だから、だったんです。
当時のパソコンはとても非力だったんです。そして元々Lisp系言語は大容量のメモリとCPUパワーを必要とする言語で、「大型コンピュータ」でしか動かなかった、んです。パソコンで扱えるような代物じゃなかった、んですね。




大型コンピュータの例



PDP-11


名機、と呼ばれるミニコン(ミニ、とは言ってもコッチの感覚ではやっぱり巨大)である。大学/研究所で絶大な人気を誇った。これはまだ古い機械の部類だが、これを生産してたDECと言うメーカーはこの後もこのテの大型コンピュータを作り売りつづけていた。
なお、DECはそのうちCompaqに買収されて、そのCompaqもヒューレッド・パッカート社に買収された。


この辺で、「何故Lisp系言語が知名度が低いのか?」その理由も分かるとは思います。歴史的には、大型コンピュータを所有出来るような大学、なんてのは数が限られていましたから。国家から購入予算をぶんどってこれる大学と言えば……。そう言うわけ、なんです。そして当然東京工業大学なんてのはそう言う数少ない大学なのは間違いありません。
21世紀に入ってパソコンの能力が飛躍的に上がって過去の大型コンピュータの能力を遥に凌駕しています。要するに現代は、中学生〜高校生辺りでも「自宅で楽々Lispを扱えるようになった」空前絶後の状態なんですね(考えてみれば、亀田が90年代に人生初めて購入したパソコンなんかは、今ではUSBメモリに丸まる入ってなお余ります・笑)。
一般人の逆襲はこれから、です(笑)。パソコンの能力が桁違いに上がった以上、もはや大学/研究機関じゃなくっても彼らが愛用している「便利ツール」は自宅でも楽々扱えるのです。

取り合えずこのテキストの特徴と言えば「Lisp系言語っぽい説明を」「Pascal(を模した疑似コード)で無理矢理やろうとしている」って事です。これを把握しておいてください。解説がチグハグで、プログラミング未経験者が「分かりづらい」って思うのは当然、なんです。
多分このセンセもLispで書けたら書きたかった、ってのが本音なんじゃねえのかな、とか思います。時代がそれを許さなかった(そしてパソコンではPascalが当時はそこそこ人気があった)、って事なんでしょう。

では、再帰はPython、Ruby、R辺りでも使えるのか?使えます。元々Lisp系言語の影響受けてるんで、これらの言語はこう言うのも大得意、なんですよ(笑)。ちょっと書いてみましょう(Schemeが苦手だ、って人はこっちの方が分かりやすいでしょうし)。




# -*- coding: utf-8 -*-
# Pythonの場合

def Summation(n):
if n < 0:
raise ValueError
elif n == 0:
return 0
else:
return Summation(n - 1) + n # この部分が再帰

# Rubyの場合

def Summation n
if n < 0
raise
elsif n == 0
0
else
Summation(n - 1) + n #この部分が再帰
end
end

## Rの場合

Summation <- function(n) {
if (n < 0) {
return(NA)
}
else if (n == 0) {
return(0)
}
else {
return(Summation(n - 1) + n) # この部分が再帰
}
}


こんな感じですね。何を行っているのか、もう分かりますね?「現在設計している関数内」で「作っている最中の関数を(nを一個減らして)呼び出している」のです。
比べてみると……この中で一番シンプルなのはRubyかしら?Pythonだと明示的にreturn噛まさないとエラーになるんですが、一方Rubyではへっちゃらけのけ、ですね。コードの見てくれ、そしてタイピング量は結果一番短いと思います。Rもまあまあ、ですね(C言語っぽさは、やっぱり個人的には好きじゃない、んですが・笑)。
そして、最初の方に掲げた「Summation」、あれらは通大のテキストのPascalのコード例にわざと合わせた、んですが、こっちの「再帰版」の方が遥に短くシンプルに書けてるのが分かると思います。もちろん入出力の分も引数と返り値で解消しているんで当たり前なんですが、forループ系を使うよりメンド臭くないのが分かるでしょう。数学的定義を厳密に置き換えれば一丁上がり、なのですから。ループであれこれ悩む必要が無い、のです。
前の方で「一生何らかの形でプログラミング言語と付き合っていける事を目標とする」と書きました。んで、別にここに掲げた言語じゃなくても構わないんですけど、プログラミング言語を選ぶ際、大事なポイントが一つだけあります。世の中にはオブジェクト指向言語であるとか、新しくはアスペクト指向言語とか、まあ、色々あるんですけど、その辺はどうでも良くって、一番注目しなければならない事は次の一点です。それは

再帰を難なく扱えるプログラミング言語が良い言語

です。再帰を扱うのが難しいプログラミング言語はそもそも勉強するに値しません(例えばVBAとか)。これは絶対的な鉄則です。
何故なら、高級言語を使う目的は「短くコードを書けるから」の一点なんですよ。ここの数列の和の例を見れば分かる通り、forループを使うより再帰を使った方が簡単なんです。これは一体何を表しているのか?
反復ってのは「コンピュータは得意」ですが、元々人間は「反復が嫌い」なのです。従って、「反復を考える」事自体が苦痛です。書くのも嫌ですし、そもそも人間の思考回路としては「反復は不自然」なのです(だからプログラミング初心者にとっては、ループを書く事が一番難しく見えるのです)。
再帰を使う方が単純で済むのに「ループ構文を書かなければならない」と言う事は、これはそもそも人間が「ループ構文を書く」事によって一種「(プログラミング言語の為の)人間コンパイラになっている」って事ですね。「機械が得意な事は機械にやらせれば良い」のだったら、人間が反復を考えるんじゃなくって、「勝手に反復に翻訳してくれる」プログラミング言語を使った方が良い、と言うのは自明でしょう。
よって、プログラミング言語を選択するにあたって一番重要なのは「再帰を簡単に書けるかどうか」をチェックする事になるんです。


注:なお、これが人間の不思議なところなんですが、先にも書いた通り、プログラミング初心者が、必ず最初に躓くのがとにかく「反復」なのです。しかし、「反復をガリガリ書く」練習をしてしまうと、今度は「反復は簡単だけど再帰は難しい」と言う風に概念が「逆転」してしまうんです。慣れってのは恐ろしいモノです(笑)。
事実、多くのプログラミング言語の入門書ですと、最初に「反復」をガリガリ練習させておいて「反復思考の不自然さ」に初心者を慣れさせるんですが、反面、再帰の取り上げ方は「プログラミングの奥義」「難しい概念」っぽい扱いになっています。繰り返しますが、これは本来は逆なんです。再帰の方が実は簡単に書けて、スピードを稼いだり、メモリ効率を良くする為に、後により難しい反復で書き直すのが本来は正しいのです。こっちの方が順番から言うと人間にとって「優しい」のは間違いありません。
では、どうしてこう言う悪習(反復の後の再帰)が根づいたままなのか、と言うと、元々再帰の方が、工学的に「コストがかかってた」からなんです。過去のハードウェア上の制限への対応が今も引きずられているから、に過ぎません。そう言う古い時代にプログラミングを覚えた人たちがプログラミング入門書を書くと、やっぱり「反復の方が簡単だ(慣れちゃったから)」「再帰はコストがかかる(今じゃ大したコストじゃないのに)」と言う思考から抜けきれないまま初心者に語る羽目になるわけです。確かにそれは、ある種のプログラミング言語(例えばC言語やPascal)では「正解」ですが、その他では「それほどでもない」無駄な「方便」になってしまってるのです。
と言うわけで、ここではより自然な「再帰」に最初に慣れるべきだ、って事を言っておきます。特にプログラミング初心者こそ「再帰」を最初にやるべきです。少なくとも作成すべきコードと数学とが一対一対応になりやすいんで「ややこしい思考の強制」は極力減ります。そして「徐々に」反復に慣れていった方が遥に「学習がラク」です。これが「Lisp系言語の知恵」なのです。



さて、こう言う風に、原理的にはここまでは背後ではLispっぽい「再帰」ベースで解説されてるのが通大のテキストなんですが、これ以降の疑似コードでPascal調の「疑似プログラム」が提示されています。今までの話を鑑みながら読んでみてください。どっちがより複雑で技巧的なのか、分かると思います。


これまでをまとめると、目的のプログラムは次のようになります。

  • プログラム  「1からnまでの和」

    • 変数 n, k, 和:整数型


    {

    • nの値を読み込む

    • もし (n≦0) ならば プログラム終了

    • 和←0

    • kは1からnまでとして次を繰り返す
      {

      • 和←和+k


      }

    • 和の値を出力する


    }




なお、実はPascalでもC言語でも「再帰」は可能です。ただし、一般的にメモリ管理が無茶苦茶難しくなります。
また、十進BASICなんかでも

再帰呼び出し可能な手続き定義


組込み関数にない関数は,DEF文のほか,複数行での記述が可能な関数定義を用いて利用者自身が定義して用いることができます。また,副プログラム定義を利用すれば,アルゴリズムを定義しておき,それを呼び出して利用することができます。関数定義や副プログラム定義のなかで自分自身を利用することもできるので,再帰的なアルゴリズムも簡単に実現できます。

とか書かれているんですが、どうなんでしょうねえ?
こんな感じか。



REM 十進BASICの場合
DECLARE EXTERNAL FUNCTION Summation
INPUT n
PRINT Summation(n)
END
EXTERNAL FUNCTION Summation(n)
IF n<0 THEN
LET Summation$="ERROR!"
ELSEIF n=0 THEN
LET Summation=0
ELSE
LET Summation=Summation(n-1)+n
END IF
END FUNCTION

まあ、出来なくはないけど……。何じゃこりゃ、ゴチャゴチャしてて汚ねえなあ(爆)。
大体、外部関数定義、って何やねん(苦笑)。そもそも何で「必ず」入力命令経由しなきゃなんないんだか(笑)。不自由過ぎるにも程があります(苦笑)。
一つ提案。上の十進BASICヴァージョン見れば分かりますが、、一応十進BASICでも再帰は可能なようなんで(ホント一応、ですが)レポートやテストを「全部再帰で書いて」提出してやれや(笑)。
いくら何でも「91年製のテキスト」を現行で使いつづける、ってのはダメとしか思えないので、そう言う「反逆」もアリだよ、って事です(笑)。何せ「動くことは動く」んで減点のしようがありません(笑)。
そう言う形で、

「再帰はメモリ効率が悪い」

とか言ってきたら

「僕等、メモリ効率とか習ってませんから」

とか言い返せます(笑)。そもそも「再帰がダメ」とか減点してくる教授がいたら、そいつら本当に「ダメ教官」で確定ですし(笑)。いや、ホント、マジな話ですよ、これ(笑)。
まあ、もっともPython、Ruby辺りやったら十進BASICで何が何でも書きたい、とか思わなくなるでしょうけどね。「どうせだったらCかPascalの方がマシっぽい」とか思うようになるとは思います。その辺がレポートで「使いたい」って思う言語になるでしょうね。





[課題1・2] 次の一般項を持つ数列{$a_{k}$}の、第1項から第n項までの和を求めよ。

$a_{k} = k^2 + \sqrt{k}$、k = 1, 2, 3,・・・


もうこの辺も、前問題解ければ別段難しくないでしょう。
「整数型」も「実数型」もこれらは静的型付け言語の話なんで、動的型付け言語には一切関係ありません。まずは「ロジックが正しくて」「動くことは動く」と言う事を確認してから、C言語やPascalに「どうやって翻訳しよう?」って考えれば良い。ロジック自体はどんな言語にも移植可能なわけですから、それ以外の「言語特有の部分で」初めて「型」を考えれば良いのです(しかも十進BASICでは型を考えなくて済むし)。最初に知らなければならない事は、「どんな結果が返るか?」です。
その辺に「動く疑似コード」を使う利点がある、のです。




# -*- coding: utf-8 -*-
# Pythonの場合

import math # これがPythonの場合必要

def Summation(n):
if n < 0:
raise ValueError
elif n == 0:
return 0
else:
return Summation(n - 1) + n ** 2 + math.sqrt(n)

# Rubyの場合

include Math # これがRubyの場合必要

def Summation n
if n < 0
raise
elsif n == 0
0
else
Summation(n - 1) + n ** 2 + sqrt(n)
end
end

;; Schemeの場合

(define (Summation n)
(cond ((< n 0) (error n))
((zero? n) 0)
(else (+ (Summation (- n 1)) (expt n 2) (sqrt n)))
))

## Rの場合

Summation <- function(n) {
if (n < 0) {
return(NA)
}
else if (n == 0) {
return(0)
}
else {
return(Summation(n - 1) + n ^ 2 + sqrt(n))
}
}


さて、一つだけ注意点があります。
PythonとRubyの場合、(ある意味)欠点があって、それは「平方根を取る」と言う簡単な数学的操作でさえ、「数学関係のライブラリ」をインポートしないと使えない、と言う事です。
この辺、言語設計者の好み、ってのもあるんですが、ある主義によると「言語の中核部分はなるべく小さくしたい」と言う流儀があるようで、Python、Rubyはその流儀に従っています。
確かに、通常のプログラミングに於いては「数学的な計算」ってそんなにやらないんですよ(笑)。そこで、Python、Rubyは「数学関係の機能」を本体から切り離しちゃっている、のです。よって、数学的計算をやる場合、明示的に「数学関係のライブラリを使います」と宣言しないとならないのです。
反面、Schemeは数学出身なんで、数学的関数はたくさん揃っています。また、Rは元々「統計解析用」なんで、当然数学的関数はそのままで使い放題なんですね。
平たく言うと、どうして高校数学の窓で紹介する言語としてPythonが最終候補から落ちたか、と言うと理由はそんなトコ、なんです。いちいち毎回import mathって書くのがメンド臭かったから、です(笑)。
(ここ、良く覚えておいてください。「メンド臭い」って言うのがプログラミングに於いて、如何に強力な理由になるのか、を・笑)
まあ、Python、Rubyは「数学には恒常的に使うにはメンド臭い」って思っただけで、日常用のプログラミング言語として、とか、情報工学系の初心者用言語としては、いずれにせよ優れているとは思います。

さて、余談ですが、こう言う「機能」の「本体との切り離し」、ってのはプログラミング言語設計に於いて、結構良く見かけるんです。
例えば。
一番典型的な例としては、やっぱりC言語がありますね。こいつはクセモノで、何と「本体だけ」では入出力機能さえ無いんです。
C言語で有名な「一番最初に書くべきプログラム」として、通称「HelloWorld」プログラム、って言うものがあります。単純に"Hello World!"って画面に表示させるだけ、のしょーもないプログラムですね(笑)。
典型的なHelloWorldプログラムは次のように書きます。



#include <stdio.h> /* ここだ!!!これは一体何だ? */

int main(void)
{
printf("Hello World!\n");

return 0;
}

まあ、大体、プログラミング初心者はこのコード見るだけでも「わけが分からない」って不安に陥る、んですよ(笑)。どーしてたかが"Hello World!"って表示させる為だけにこんなにいっぱい書かなきゃならんのか?と。
やはり冒頭ですよね。ここです。



#include <stdio.h>

これで「入出力関係のライブラリをインポートします」って言ってるわけなんです。何せ、Cには入出力機能がない。先に見た、「Python、Rubyには数学的機能が本体には無い」と言うのと対比してみれば良く分かるでしょう。「必要となるライブラリは明示的に呼び出す」ってのがある種のプログラミング言語の流儀、なんです。
ちなみに、一番簡単なCのプログラムは次のようなプログラムです。



int main(void)
{
return 0;
}

酷いでしょ(笑)?このプログラムは実は「何もしない」プログラムなのです(笑)。入出力が使えない、となると「これくらいしか」やる事無いんですよ。それは「何もしない」事です(笑)。
まあ、いずれにせよ、この「ライブラリのインポート」と言うのは、どのみちいつかは経験する羽目になるんで(例えばC言語とか、で)、Python、Ruby組は「数学やるときは数学ライブラリをインポートする」と念仏のように唱えてても良いでしょう。ここでもいずれ、もっと多用する羽目になるでしょうから。

さて、そこまで了承して、以降は流していきます。
上の4つの「走る疑似コード」をどうやってPascal(あるいはC言語)へと翻訳するのか、と言う説明だと考えれば以下はそれほど難しくもありません。結構重要なのは「型に関する」説明ですね。
また、Pascalとかが「如何にメンド臭い言語なのか」大変分かりやすい解説になっている、と思います(それはC言語に付いても、です)。


1段落目の問題の分解は、課題1・1と同様に、

  • nを入力する

  • $a_1 + a_2 + \ldots + a_n$を計算する

  • 上の計算結果を出力する


となります。当然、問題となるのは2番目の命令です。$a_k$の値を求めることと、その値のk=1からnまでの和を求めることとは別々に分けて考えることができます。そこで、$a_k$の値を計算する実数値関数a(k)があるとすれば、実数型変数"和"を導入して、この課題のプログラムは、次のように書けます。


  • プログラム  「数列の第n項までの和」

    • 変数 n, k : 整数型
          和 : 実数型


    {

    • nの値を読み込む

    • もし (n≦0) ならば プログラム終了

    • 和←0

    • kは1からnまでとして次を繰り返す
      {

      • 和←和+a(k)


      }

    • 和の値を出力する


    }



関数a(k)の仕様は問題の中に与えられています。したがって、そのサブプログラムをすぐに次のように書くことができます。


  • 関数 a(k : 整数) : 実数型
    {

    • $a \leftarrow k^2 + \sqrt{k}$


    }



前のメインプログラムとこの関数のプログラムを一つにして、課題1・2のプログラムとなります。
上の関数a(k)のプログラムは、なにも問題ないように見えますが、実引数に負の整数が与えられたときは、正しく機能しないことに注意してください。負の平方根は存在しません。今の場合、これを呼び出すメインのプログラムでは、実引き数として負の値を与えることは確かにありません。しかし、この関数を他のプログラムで流用したりする場合も考えて、メインプログラムのnの値の判定と同様に、引き数が本当に非負の値であるか検査する部分を追加することにより信頼性を増すことにします。このような姿勢によって、プログラムをコンピュータで実際に動かしたときでるエラー(実行時エラー)を減らすことができます。そこで、上の関数のプログラムを次のように書きかえることにします。


  • 関数 a(k : 整数) : 実数型
    {

    • もし ($k\le 0$) ならば プログラム終了

    • $a \leftarrow k^2 + \sqrt{k}$


    }



この節では、同じ変数に繰り返し代入することで数学的にはΣを用いて表されるものを計算できることを学習しました。

<練習問題>

  1. n!(nは非負の整数)を求めるプログラムを作成せよ。

  2. 1.のプログラムにおいて、nが大きいとき整数型の最大値32767を超えるが、どのように処理すればよいかを考えよ。




<回答例>

1.においては0!=1である、と言う数学上の約束さえ思い出せば、今まで書いたプログラムを改造してすぐ作成可能です。
これもプログラミングの学習に於いて「再帰の例」としては良く使われます。
(つまり、通大のテキストのように「繰り返しで」書く事は滅多にありません。)




# -*- coding: utf-8 -*-
# Pythonの場合

def Factorial(n):
if n < 0:
raise ValueError
elif n == 0:
return 1
else:
return Factorial(n - 1) * n # ここが再帰

# Rubyの場合

def Factorial n
if n < 0
raise
elsif n == 0
1
else
Factorial(n - 1) * n # ここが再帰
end
end

;; Schemeの場合

(define (Factorial n)
(cond ((< n 0) (error n))
((zero? n) 1)
(else (* (Factorial (- n 1)) n)) ; ここが再帰
))

## Rの場合

Factorial <- function(n) {
if (n < 0) {
return(NA)
}
else if (n == 0) {
return(1)
}
else {
return(Factorial(n - 1) * n) #ここが再帰
}
}


まあ、試しにforループとかwhileループで書いてみてください。練習代わり、としては良いでしょう。
2.に関して言うと、動的型付け言語を使う以上「そんな事知るか!!!」です(笑)。その辺はプログラミング言語の方で回避出来る範囲では回避してくれるでしょうから(笑)。
真面目に考えると次の方策が考えられます。

  • 単精度の整数型ではなくって倍精度の整数型を宣言して使う。

  • あるいは、32767を超える最小のnを先に計算しておいて、そのn以上の数値が入力された場合、プログラムが停止するかエラーを返すかするように仕込んでおく。


辺りでしょうね。
なお、本格的な数値計算に於いては、

$$n! = 1 \times 2 \times 3 \times \ldots \times n$$

と言うのはなかなか大変な計算なのは確か、です。すぐ整数が巨大化してしまう。
こう言う場合、トリックとしては対数を取って、

$$\log{1} + \log{2} + \ldots + \log{n}$$

と和の形にしてしまう事が良く行われています。
と言うのも、

  1. 浮動小数点型である対数の方が、数値の増加率を抑えられてコンピュータで扱いやすい。

  2. 一般的にコンピュータでは、乗算よりも加算の方が計算スピードが速い。


と言う特徴があるから、なんです。必要になったら指数で元の形に戻す、と言うトリックを良く使います。
ここではそれを書きませんが(と言うか、すぐ書けるでしょう)、興味のある人は実装してみて下さい。


※:あるいは単に「漸化式」って言った方が通りが良いかもしれません。
なお、「再帰の方が簡単だ」と再三書いてますが、これは本当です。と言うことは、主張しているのは「漸化式が簡単だ」と言う事です。果たしてこれは本当か?数学が苦手な人間でも「漸化式が簡単だ」と思えるのかどうか?
結論から言ってしまえば「その通り」です。実は数学が苦手な人間でも漸化式自体にはあまり抵抗を示さないもの、なのです。

ウッソ〜〜〜〜!!!!!

と言う声が聞こえてきますが(笑)、本当です。
高校教育現場等で、数列が苦手、な生徒は事実大量に存在しているでしょうし、「漸化式なんか点数取れない分野だよ」って思う人もいるでしょう。はい、それは事実だと思います。ただし、そこに勘違いがあります。
「数学が苦手な人」が嫌なのは、「漸化式そのもの」が嫌いなんじゃなくって、「漸化式を"解け"」って言われるのが嫌、なんです。この二つは全然意味が違います。「解け」って言われた段階で、Σが出てきたり、計算が技巧的でメンド臭かったりで「嫌だ、と感じる」のが本当のところなのです。
十中八九、「漸化式の"意味"を述べよ」とか「書け」とか言われたら割にスラスラ読めるだけは読める/書けるだけは書ける、って生徒の方が多いだろ、と思います。連中は「解く」のが嫌なだけ、なんです(笑)。
これを鑑みると、「再帰」って方法論は「漸化式を設定して」、"解く"作業はプログラミング言語に丸投げ、って方法論です。一方、「反復」はΣの中身を想像して「動作を書き下す」と言うのが行われている内容です。果たしてどっちが楽なのか?と言うと「漸化式を作るだけ作っておいてあとは知らんぷり」の方が圧倒的にラクだろ、と思います。

疑似プログラム


コンピュータは道具です。古くからある単機能の道具と異なり、いろいろなことができます。ただし、それが可能となるのは人間が何の目的で使うか考え、さらにそのための実現手段を考えだし、それを正確に順序正しく命令としてコンピュータに与えたときのみです。そのとき、あいまいな表現は許されません。いままでの説明では、自分で考えることができないコンピュータに対して、命令を与える上で知っておかなければならないいくつかの基本的な要素について述べてきました。
ここでは、それらの要素を使って、ある目的を実現するための、手順、段取りを表現する、すなわち、疑似プログラムの記述の仕方の大枠を示します。そして、次の章では具体的な問題に対するプログラムを、その形で書き表すことにします。

  • プログラム  「○△□○△□○△□」

    • 定数
      Pi = 3.1415 : 実数型
      e = 2.7182 : 実数型
        ・・・
        ・・・

    • 変数
      x, y : 実数型
      i, j : 整数型
      v : 1次元実数型配列 [1.. 3]
      A : 2次元実数型配列 [1.. 5 ; 1.. 3]
          ・・・
          ・・・


    {

    • 命令文1

    • 命令文2

    • 命令文3

    • ・・・

    • ・・・

    • 命令文x


    }



これをメインプログラムのひな型とします。
プログラム名は、これがなんのプログラムであったか、後になってもすぐに分かるようにするためにもなるべく意味のあるものにする必要があります。コンピュータにやらせる仕事の目的が明らかになった後、実際の処理の対象となる情報のデータ構造をどうするかという問題を考えなくてはなりません。それを解決したならば、すべてのデータを格納する変数を、そのデータ型と共に宣言します。次に、やらなくてはいけない処理を順に命令文として書き出します。メインプログラムにおける処理の記述は、大まかであるのが普通です。複雑な処理は、階層化することにより、関数または手続きとして呼び出します。

サブプログラムは、メインプログラムの後に続けて書くことにします。最初の段階では、サブプログラムの部分には仕様だけを記述する場合もあります。全体の見通しをたててから、その仕様に沿って詳細化を行うことを考えます。どの段階まで詳細化を行えばよいかは、状況によって異なってきます。最初からある特定の言語に直すことを想定しているならば、その言語の基本命令まで詳細化しなければなりません。このテキストでは一般的な数学表現のレベルでの詳細化を考えます。


何度も言ってますが、

ここでは、それらの要素を使って、ある目的を実現するための、手順、段取りを表現する、すなわち、疑似プログラムの記述の仕方の大枠を示します。そして、次の章では具体的な問題に対するプログラムを、その形で書き表すことにします。

それがそもそも大間違いだ、っての(苦笑)。
次に、ひな型である通大用語の「疑似プログラム」なんですが、本来疑似コードでは、

  • プログラム  「○△□○△□○△□」

    • 定数
      Pi = 3.1415 : 実数型
      e = 2.7182 : 実数型
        ・・・
        ・・・


    • 変数
      x, y : 実数型
      i, j : 整数型
      v : 1次元実数型配列 [1.. 3]
      A : 2次元実数型配列 [1.. 5 ; 1.. 3]
          ・・・
          ・・・



    {

    • 命令文1

    • 命令文2

    • 命令文3

    • ・・・

    • ・・・

    • 命令文x


    }


横線引いた部分は「要らない」部分です。どう考えてもこれは「Pascalの流儀」なんで、この辺の宣言は「要らない」のです。大体、宣言やる限り、「その言語のデータ型を全部知らないとならない」し、かつ「他の言語でのデータ型が同じだ」と言う保証が無いんで、ポータブルじゃないんです。
もうこの辺は、例えばPythonやRubyを選んだ人は分かってきている、でしょう。ああ言う言語自体が構造的には既に「疑似コードである」と言う事を。

プログラム名は、これがなんのプログラムであったか、後になってもすぐに分かるようにするためにもなるべく意味のあるものにする必要があります

う〜〜ん、この辺りは「もっともだ」とは思うんですけど……。
いや、実は一概には言えないんですが、一般に「名前の付け方」ってのは大きく分けて2系統あるんです。これらは「コンピュータの発展」に寄与してきた2大潮流があったから、ですね。

  • プログラムの名前を極限まで短く書く事を良し、とするAT&T方式

  • プログラムの名前を親切に長く書く事を良し、とするMIT方式


もう一度解説しますが、AT&TってのはUNIXやC言語を開発した(本業は電話)会社で、彼らが「記号化と思えるほど短い」名前を付けるのを好みます。

短いと名前としては分かりづらくて良くないんじゃない?

と思うでしょう。確かにそうではあるんですが……。
一般に「良く利用する」プログラムだと、文字数が多いと「頻繁に呼び出すのがツラい」んですね。また、「長い」とタイピングミスを誘発する可能性が高くなる、んです。
例えば、どんなプログラムかは問いませんが「高校数学の窓」(Window of High-School Mathematics)と言うプログラムを書くとします。
このプログラム名として

whs

なんて言うプログラム名を付けるのがAT&T方式です。そして、別のプログラムでこのwhsを10回呼び出すにせよ、whsとアルファベット3文字を記述するのはそんなに大変ではありません。
実際、AT&Tで開発されたUNIXのコマンド(プログラム)名は、殆ど原型を留めていないくらい省略されています。それがまた初心者泣かせなんですが(笑)、一方慣れてしまえば総体的にタイピング量が減り、それはそれでとても有効なんです。
AT&Tの発想としては

人間は元来タイピングが嫌いである。

と言うアイディアが背景にはあるんでしょうね。
一方、通大のテキストに書いてあるような方式がMIT方式です。MITはLispを開発した世界最高峰と言って良い程の理工系大学なんですが、そのLispで使われている関数名は一般的にとても長ったらしいものです。
例えば、「高校数学の窓」(Window of High-School Mathematics)と言うプログラムを書くとすると、そのまま

window-of-high-school-mathematics

と名づけちゃうのがMIT方式ですね。
確かに「名前としては」分かりやすいんですが、逆に何十回もタイピングするとなると殆ど「苦行」です(笑)。また、タイピングミスも起こりやすい、って事は言えます。
MITの発想としては、

人間が全てタイピングする必要は無い。そう言うメンド臭い事こそ、コンピュータにやらせるべきだ。

と言うアイディアがあるようです。実際、MITはこの辺の「開発支援環境」の作成には定評があり、例えばwin辺りまでタイプすると「window-of-high-school-mathematics」をコンピュータが候補として返してくれる、いわゆる「入力補完機能」付きの開発環境作りはMITのお家芸で、事実上、これを倒したプログラムは存在しません。
一方、その「開発環境自体」が使うのが難しかったりして(笑)。いやはや、MITは一筋縄では行きませんよ(笑)。
まあ、いずれにせよ、「名付け方」はなかなか難しい問題です。
亀田の提案としては「名付け方」はある意味どうでも良いんで、どっちかと言うと「コメント」を丁寧に記述しておき、後で「コメント」を読んで「どんなプログラムなのか」分かるようにした方が良いかな、とか思っています。

サブプログラム


大きく複雑な問題があったとき、まず、その問題をよく分析して、いくつかのより小さな、より単純な互いに独立な問題に分割します。それらの小問題を順番に解決していけば、大きな問題を解決したことになります。
プログラム作成でも、このような方法で取り組むのが普通です。これを補助するものとして、サブプログラムというものが存在します。

これも最初の方で書きましたが、現代的な観点では「モジュール化」と言う言い方の方がしっくり来るでしょう。
なお、元々C言語なんかでは、確かにmainと呼ばれるプログラムと「それ以外」のプログラムがあって、「それ以外」はサブプログラム、と呼んでも良いかもしれません(呼ばないでしょうがね)。
一般に、C言語等のコンパイラが中心の言語(※)では

  • 特定の"主"プログラムしか端末から呼び出せない。

  • 従って、個々の「小さな」プログラム単独では実行出来ない。


と言うような設計が成されています。従って、明らかに主従関係が存在するんですが、一方、現代的な言語では個々のプログラムは全て「単独で動く」ので、事実上、主従関係は形式的には存在しない、のです。
ここで紹介した4つの動的言語は全て「現代的」なものです。

※:ちなみに、ANSI(アメリカ国内標準規格)に従う限り、 C言語では特にコンパイラとしての動作が仕様書で定義されているわけではないようで、理論的には「Cのインタプリタ」は存在しうる。従って「Cはコンパイラ型の言語で〜」と言う説明は原理的には間違っている。
また、理論的には、ANSIで制定された言語のうち、コンパイラとしての動作がきちんと定義されているのはANSI Common Lispしか存在しないようだ。
なお、「ANSIはアメリカの基準でしょ?日本国内には関係ないんじゃない?」と思われるかもしれない。事実、日本国内ではJIS、と言う独自規格がちゃんと存在してはいるが、ことプログラミング言語に関して言うと、JISで制定されたものの中身の殆どはANSIのパクりである。



引き数


サブプログラムは、ある問題(上位の問題)を分割した小問題(下位の問題)の解決手順および段取りを書いたプログラムです。当然、いくつかのある小問題のなかで、どの小問題に対するサブプログラムであるかを明らかにするために、そのサブプログラムに名前(サブプログラム名)をつけて区別することになります。上位にある問題のプログラムでは、下位にある問題のサブプログラムの名前を呼ぶだけで、その問題は解決されます。


ここで重要なのは、上位、下位、と言う概念はどもかくとして

プログラム同士はお互いに呼び出しあっても良い

と言う事を知る事、です。


主問題のプログラムの基本要素の中に、コンピュータと外部世界との情報のやり取りの部分である入力と出力があります。サブプログラムでは、それを呼び出すより上位のプログラムと情報をやり取りしなければなりません。また、サブプログラム同士でも情報をやり取りする必要が生じるときもあります。
サブプログラムは、それ自身、独立したプログラムであり、独立した世界です。他の世界、すなわち他のプログラムとの接触は、必要最小限におさえなくてはなりません。そのために、他のプログラムと情報をやり取りする場所は、一ヶ所にまとめられ、さらに限られたものに限定されます。これは、江戸時代、幕府の鎖国政策において唯一長崎出島でオランダ人、ポルトガル人を通してのみ外国との接触が認められていたのに例えることができるでしょうか?

出島(笑)。「例えることができるでしょうか?」とか言われたってねえ(苦笑)。上手い例えなんだか下手な例えなんだか(笑)。
一つ言えるのは、「主問題のプログラムさえ」与えられる情報は制限されてる、って事です。別に出島、じゃなかった(笑)、玉川用語で言う「サブプログラム」に限った話ではない、って事です。この説明だと「主問題のプログラムだったら」どんな情報でも受け取れる、とも読めますね。んなこたあない、です。


サブプログラムにおいて、出島のオランダ人、ポルトガル人、すなわち外国の情報を持ってきて日本の情報を選び出すものにあたるものが引き数といわれるいくつかの値や変数です。サブプログラムは、この引き数といわれるもののみを通して他のプログラムと情報を交換します。サブプログラムを記述する段階では、引き数となる具体的な値や変数は決定されていません。ただし、それらのデータ型は確定しているものとします。そこで、サブプログラムを書く段階では、仮引き数と呼ばれる、実際に情報をやり取りするものの代用品を用います。

下線部:亀田


ほら、またそう言うウソ書く〜〜〜。
通大用語で言う「サブプログラム」と「引数」(通大テキストでは"引き数"と記述される)を無理矢理結びつける必要、って無いでしょうが。これじゃあ「引数」は「サブプログラムでしか使えない」ように読めちゃうでしょ?
(あるいは、Pascalがそう言う実装なのか?良く知らんけど)
主目的のプログラムでも引数は使えます。C言語なんかでも「コマンドライン引数」ってのありますよね?

「でもプログラミング初心者用テキストだし、コマンドライン引数は難しいから。」

とか言う言い訳聞こえてきそうですが、だったらなおさらこう言うシチメンド臭い解説はしないに越した事無い、です。
これ以降の解説の方が遥にマシです。

数学的な例で、これを見てみます。数学で関数f(・)を定義するとき、

f(x) = 3x + 1

という式で表現することがよくあります。ここで、変数xを用いましたが、それを変数aにかえても、変数zにかえても定義される関数は同じものです。このとき、このxが仮引き数になります。また変数xは、ここでは明記されていませんが、実数であることは暗黙のうちに仮定されています。プログラミングにおいては、原則としてこのような暗黙の指定はないものと思ってください(プログラミング言語によっては暗黙の宣言を認めているものもありますが)。関数f(・)をつかうことにより、f(2)と書けばそれは3×2+1を計算した値を意味します。今の場合、この2が実際の引き数(実引き数)となります。


下線部:亀田


プログラミング言語によっては暗黙の宣言を認めているものもありますが

そうですね。僕達は既にそう言う「動的型付け言語」を用いて、このテキストを読み始めています。

さて、1ページ以上に渡る説明の殆どは無意味で、要するに引数とは、

数学的関数をf(x)とした時、数学上はxを「独立変数」と呼ぶが、プログラミング用語では「引数」と呼ぶ

それだけ、なんです。大した話じゃあありませんし、中学生にも理解出来る話、です。
よって「サブプログラム云々」ってのはどーだって良い話、なんです。


関数と手続き


通常、サブプログラムは関数と手続きの2種類に分類されます。この2つは、共に一連の命令が記述されたプログラムであることにかわりはありません。関数は数学におけるのと同様に、なにかの値を計算しその値を返すことを主とします。それに対して、手続きはそれ以外のなにかまとまった仕事をするということが中心となります。

関数は呼び出されると、なんらかの値をその関数名で返します。これはデータを別のデータに変換することを意味します。ある関数が別のプログラムで呼び出される場所は、その返される値を使うために式の中でなければなりません。手続きは、関数と異なり値をその手続きの名前で返すことはありません。しかし、それを呼び出されたならば、なんらかの仕事をし、場合によってはその仕事の成果をもって帰ります。普通、その成果は引き数の中の一つの変数の中に納められています。


冒頭の

サブプログラムは関数と手続きの2種類に分類されます

から始まって、ツッコミどころが多過ぎるんだよな〜〜〜(苦笑)。

まあ、少なくとも言えるのは、これはプログラムの一般論、と言うよりは「半分以上が」Pascalの解説です。だからホントに困ったちゃん、なんですよ(笑)。

まず、工学的には、確かに次の区分けが存在している事は事実なんです。

  • 関数:計算した「結果」を返すプログラム

  • 手続き(プロシージャ):「計算」自体と何も関係無いプログラム


ここで「計算自体と何も関係無い」自体を定義する事が難しいんですよね(笑)。例えば、「表示」なんかが典型例ですよね。例えば「1」と言う数を受け取ってその「1」を画面に表示する。この場合、「1」自体が計算されて何かに変化するわけじゃありません。こう言うのを専門的には副作用って呼ぶんですが、出力命令はこの副作用の典型例なんです。
一方、「1」を受け取って「1」をどっか別のプログラムに受け渡す、場合、これは定義的には「計算を伴う」のです。この「受け渡した値」を専門的には「返り値」と呼びます。
また、このケースの場合、数学的には関数として

f(x) = x

ですから、確かに受け取った値の「1」は関数f(x)によって「1」と計算されているわけです。
上の二つの命題は、言い換えると、

  • 関数:返り値を目的としたプログラム

  • 手続き(プロシージャ):副作用を目的としたプログラム


となるんですが……。

慣用的な意味に於いては、「関数」も「手続き」も同じものなんです。別な言い方をすると、厳密に「関数」と「手続き」を区別した表記法を要請しているのは、知ってる限り「Pascalだけ」なのです(んで、正直な話を書くと、通大対策で「Pascalの入門書」に目を通した際、そんな分け方をしてる事を知ってぶっ飛びました・笑)。

一般的には、プログラミング言語に於いて、作成されたプログラムを「関数」と呼ぶか「手続き(あるいはプロシージャ)」と呼ぶかは、単純にその「プログラミング言語作成者」の好み、なんですよね(笑)。慣用的には、用語として「厳密な分け方をしている」ってわけでもないのです。
例えば、C言語では「どんなプログラムを書いても」、関数、と呼びます。これはC言語の作成者であるリッチー博士が「関数と呼びたかった」んでしょう(笑)。
ここで紹介した動的型付け言語でもそれぞれ特有の呼び方をします。

  • 「関数」と呼ぶ言語

    • Python

    • R



  • 「手続き」(プロシージャ)と呼ぶ言語

    • Scheme




Schemeなんか結構デタラメで「関数型言語」のクセに「関数」とは呼ばずに「手続き」と呼んでいるのです(笑)。

注:ちなみに、「関数型言語」の「関数」とは、上の説明を見たら分かるでしょうが、「どんな処理をさせても必ず返り値を返す」事に由来します。
なお、通大「コンピュータ」のテキストのp.17辺りに「関数型言語」の解説が載っています。以下引用してみます。


関数型言語は、数学上の関数の考え方を使ったプログラミング言語です。すなわち、あるデータ(単数または複数)に作用させることによって他のデータを作り出す関数をいくつか用意しておき、これらの関数を組み合わせてプログラミングを行う言語です。したがって、純粋な関数型言語では、プログラミングはコンピュータの制御動作とは無縁になり、プログラマは関数の機能と関数・データの記述形式を知っていればよくなります。しかしながらプログラミング言語に持たせたいあらゆる機能を関数形式のみで表現することは難しい面もあり、また不自然でもあることから、純粋に関数型といえる言語はありませんが、APL、Lisp等は関数型に分類されます。


なお、これは90年代初頭に書かれた文章なんですが、21世紀の現代に於いては純粋に関数型といえる言語は存在します。→それどころか、実験的なんですけど、純粋に関数型といえる言語で書かれたこんなゲームまであります。

また、ここにはRubyが含まれてませんが、Rubyの場合は「関数」とも「手続き」(あるいはプロシージャ)とも呼ばずに「メソッド」と呼ぶ独自路線に走ってます(※)。
もうここまで来ると分かったでしょうが、「関数」と呼ぶか「手続き」と呼ぶか、は、慣用的に言うと、その言語設計者の「好み」なんです。わざわざ強調するような必要性も無い、って事なんです。

※:ちなみに、この「メソッド」は「オブジェクト指向プログラミング言語」の用語から来ていて、これが表すのはRubyは実は「オブジェクト指向プログラミング言語である」と言う事である(また、実はPythonにも「メソッド」がまた別に存在する)。
ただし、ここではオブジェクト指向は扱わないので、事実上「関数」=「手続き」=「メソッド」と単純に考えておいて間違いはない。これもやっぱり「どう呼ぶか?」は言語設計者の「好み」なのである。
しかも、実の事を言えば「オブジェクト指向とは何なのか?」厳密な定義、と言うのは存在していない。この辺もホントの事を言うと「プログラミング言語設計者」が「これはオブジェクト指向です」と言っちゃえばオブジェクト指向になる、のである。
このように、数学と違って、工学上の定義と言うのは「割にデタラメである」と言う事を肝に銘じる事。工学の流儀は「動けば良い」んで、数学者みたいに「厳密な定義」自体は好まないのである。



以下に関数と手続きの基本的な記述の仕方をみてみます。

  • 関数 Max(x : 整数, y : 整数) : 整数型

    • 局所変数 t : 整数型


    {

    • もし (x≦y) ならば t←y
           さもなくば t←x

    • Max←t


    }



これが、関数の書き方の例です。
先頭の"関数"が、このサブプログラムが関数であることを示しています。そして、Maxがその関数名になります。続く"{"と"}"で囲まれる部分に、仮引き数をそのデータ型と共に記述します。関数は必ず値を返しますから、そのデータ型を明示します。こうすることにより、変数のときと同様に関数名Maxを持つ箱をコンピュータの内部に確保します。これをもちいて、関数Maxを呼びだした他のプログラムに関数値を渡します。手続きでは、この部分は必要ありません。
さて、局所変数についての説明はいままでの中に存在しません。その名の通り、変数であることに間違いはありません。これはサブプログラムの独立性を高めるためのものです。上の場合、変数tは関数Maxを定めているこのサブプログラムのなかだけのもので、他のプログラムとはいっさいの関係を持たないことを意味します。もし、他のプログラムの中でも同じ名前の変数tがつかわれてたとしても、それはまったくの別物、別の箱となります。変数をコンピュータ内部における箱に例えたことを思い出してください。
関数を実際に定めている部分が、"{"と"}"で囲まれた部分です。関数では、必ず最後に返す値を関数名に代入します。この関数では局所変数tの値が関数名Maxに代入されています。このとき、局所変数tの値は仮引き数xとyの小さくないほうの値です。
以上から、ここで定義された関数Maxは、Max(5, 8)と呼び出されたとき、実引き数5と8のうち、小さくないほうの値8を返すことが分かります。


ええとまず、

関数では、必ず最後に返す値を関数名に代入します

ってのも大嘘ですね。と言うよりそれは「Pascalの流儀」でしょう。
全体的にこの説明は「Pascalと言う言語に於いての」説明であって、とても一般的とは思えないんですが……。
これらも実際、4つの動的言語使って、実際に書いたソースコードを提示した方が良いでしょう。

Pythonでの関数Max




# -*- coding: utf-8 -*-

def Max(x, y):
if x <= y:
t = y
else:
t = x
return t # ここがポイント

ます、Pythonでは関数を作る場合、明示的にdefで定義し始めます。これで「Maxと言う関数を定義する」と言う意味になります。
defキーワードが書かれた行末にはコロン(:)が必要で、これによりdefが形成する「ブロック」が始まる、と言う事を表しています。この辺も実は制御構造と同じ文法を採用しているのがPythonの特徴です。
後は、defキーワード以下は一段インデントを下げてコードを記述します。インデントがどう言う構造を表しているのか注目して下さい。
そして、Pythonでは明示的に値を返す場合、returnと言うキーワードを用います。これで「計算結果を返す」事を示すのです。
なお、通大のテキストではいきなり「局所変数t」が登場していますが、かなりムリクリ、と言う感じです。上のPythonのコードでは通大の「疑似プログラム」に従って、「暗黙の宣言での」局所変数tを登場させましたが、実際問題、題意としては「入力された数x、yのうち、大きな数を返せ」ってだけなんで、次のコードの方がよりPythonらしい、でしょう。


# -*- coding: utf-8 -*-

def Max(x, y):
if x <= y:
return y
else:
return x

つまり、「受け取った二つの引数のうち大きい方を返せ」って書いた方がより短くって直接的ですよね。


PythonでのMax(5, 8)の実行例



なお、実はPythonにはデフォルトでmaxと言う組み込み関数がある事を付け加えておきます。

Rubyでの関数(メソッド)Max




def Max x, y # ここはdef Max(x, y)と書いても良い
if x <= y
t = y
else
t = x
end
return t # ここがポイント
end

ます、Rubyでは関数を作る場合、明示的にdefで定義し始めます。これで「Maxと言う関数を定義する」と言う意味になります。
defキーワードが書かれたブロックの終端ではendが必要で、これによりdefが形成する関数(メソッド)が終わる、と言う事を表しています。この辺も実は制御構造と同じ文法を採用しているのがRubyの特徴です。
そして、Rubyでも明示的に値を返す場合、returnと言うキーワードを用います。これで「計算結果を返す」事を示すのです(ただし、Rubyの場合、かなり設計が優秀で、かつ「関数型言語の影響が色濃い」んで、returnは省略可能です)。
なお、通大のテキストではいきなり「局所変数t」が登場していますが、かなりムリクリ、と言う感じです。上のRubyのコードでは通大の「疑似プログラム」に従って、「暗黙の宣言での」局所変数tを登場させましたが、実際問題、題意としては「入力された数x、yのうち、大きな数を返せ」ってだけなんで、次のコードの方がよりRubyらしい、でしょう。


def Max x, y # ここはdef Max(x, y)と書いても良い
if x <= y
return y
else
return x
end
end

つまり、「受け取った二つの引数のうち大きい方を返せ」って書いた方がより短くって直接的ですよね。


RubyでのMax(5, 8)の実行例



Schemeでの関数(手続き)Max




(define (Max x y)
(let ((t 0)) ;tを0で初期化する
(if (<= x y)
(set! t y)
(set! t x))
t)) ;最後にtを返す

ます、Schemeでは関数(手続き)を作る場合、明示的にdefineで定義し始めます。これで「Maxと言う関数を定義する」と言う意味になります。
そして、Schemeは関数型言語なので、どんな場合だろうと何らかの値は必ず返します。従ってreturn等の特殊なキーワードは必要ありません。
なお、通大のテキストではいきなり「局所変数t」が登場していて、上のコードではかなりムリクリ、通大のテキストに合わせています。そして局所変数tを宣言するにあたって一応tの中身を0で初期化しています。
(通常、初期化せずに局所変数tを宣言すると、初期値がどうなるかは未定義だから、です。別に0にする必要も無いのですが。)
また、set!を使って代入してますが、通常Schemeではset!は使いません。もっと言っちゃうと「代入」なんてしないのです。
従って、次のコードの方がよりSchemeらしい、でしょう。


(define (Max x y)
(if (<= x y)
y
x))

つまり、「受け取った二つの引数のうち大きい方を返せ」って書いた方がより短くって直接的ですよね。また、極めて短いコードが書ける事が分かります(タイピング量は少なければ少ない程良い、のです)。Schemeの数学性の成せる技、です。


Schemeでの(Max 5 8)の実行例



なお、実はSchemeにはデフォルトでmaxと言う組み込み手続きがある事を付け加えておきます。

Rでの関数Max




Max <- function(x, y) {
t <- 0 # tを0で初期化する。
if (x <= y) {
t <- y
}
else {
t <- x
}
return(t) # ここがポイント
}

ます、Rでは関数を作る場合、関数名 <- function(仮引数) {と言う形で書き始めます。これで「Maxと言う関数を定義する」と言う意味になります。
関数名 <- function(仮引数) {で書き始めて最後は"}"で閉じます。この辺もC言語の影響が見て取れます。
そして、Rでは明示的に値を返す場合、returnと言う関数を用います。これで「計算結果を返す」事を示すのです。
なお、通大のテキストではいきなり「局所変数t」が登場していますが、かなりムリクリ、と言う感じです。上のRのコードでは通大の「疑似プログラム」に従って局所変数tを登場させ、局所変数tを0で初期化しましたが、実際問題、題意としては「入力された数x、yのうち、大きな数を返せ」ってだけなんで、次のコードの方がよりRらしい、でしょう。


Max <- function(x, y) {
if (x <= y) {
return (y)
}
else {
return (x)
}
}

つまり、「受け取った二つの引数のうち大きい方を返せ」って書いた方がより短くって直接的ですよね。


RでのMax(5, 8)の実行例



なお、実はRにはデフォルトでmaxと言う組み込み関数がある事を付け加えておきます。


手続きの書き方の例を次に見ることにします。

  • 手続き  3平方(a : 実数, b : 実数, c : 実数,
         OK : 論理型変数)

    • 局所変数 t : 実数型


    {

    • t ← (aの2乗) + (bの2乗)

    • もし t = (cの2乗)
         ならば OK←真
       さもなくば OK←偽


    }


これは3つの実数a、b、cが与えられたとき、次の関係式

$$a^2 + b^2 = c^2$$

が成立するかどうか判定する手続きです。成立するときは論理型変数OKが真になり、成立しないときは論理型変数OKは偽になります。
先頭の"手続き"はこのサブプログラムが手続きであることを示し、続いてその名が"3平方"であることを示します。"{"と"}"で囲まれた部分に、仮引き数を書きます。ここで、OKだけが論理型「変数」になっているのに注意してください。
一般に、手続きおよび関数において、他のプログラムからの情報は、"変数"がつかない仮引き数をとおして値だけが持ち込まれます。また、他のプログラムへの情報提供や結果報告は、"変数"がつく仮引き数をとおして行われます。"{"と"}"で囲まれた部分が、実際の仕事を記述するところです。この場合なにをしているかは、すぐに理解できることと思います。
手続きは、関数と異なり式の中で呼び出されることはありません。一つの命令文として呼び出されます。
ここで、2つの実数の間に等号関係が成立するか調べています。これは、数学的には正しいのですが、プログラミング上は好ましくありません。なぜならば、コンピュータでは真に実数全体を扱えず計算は近似で行われるため、その誤差を常に考慮しなくてはならないからです。そこで、実数型の計算をしているときの判断は不等号を用いて行うようにします。

ここは既に工学的な意味として考えても一般的な「手続き」(プロシージャ)としての解説としては破綻していますね。あくまで「Pascalでの」手続きの解説です。
まず、通大用語での「疑似コード」を見ても、原理的には真偽値が「返り値」になっていると思います。従って、ここで提示された疑似コードは「関数」なんです。
通大のテキストのスタンスとして「数値を返せば」関数で、「論理型を返せば」手続きだ、とでも言うのでしょうか?おかしいでしょう。論理値なんかもメモリ上では真=1、偽=0、と表現されているでしょうから、結果「数値として」返ってるんです。
これは副作用、を基準とした「手続き」の例示としては大嘘ですね。大体、プログラミング言語によっては「論理値」でデータを手渡したりしてるんで、要するにこの辺は「言語の設計次第」なんです。
もうこの辺になると、完全に「Pascalと言うプログラミング言語の説明」を実のトコ行っていて、「PascalではOK」なんですが、他の言語では「全然ダメな」解説ばっかになっています。ですから、「特定のプログラミング言語に限定することなく」ってのは完全に破綻していますね。だから最初に書いた通り、こんな無茶な解説するんだったら初めから「Pascalで解説」するべきなのです。そして通大生には十進BASICなんか配布せず、(Windowsでの入手は難しいので)「ソースからビルドした」Pascalを配布するべき、なのです(あるいはKNOPPIX/Mathを配布する、とかね)。

Pythonでの手続き(関数)の例


既に解説した通り、純粋な意味では通大テキストでの「手続き」の例は破綻しています。
取り合えず、3平方の「疑似プログラム」に従って「真偽値を返す」関数をPythonで記述してみましょう。


# -*- coding: utf-8 -*-

def Pythagorean(a, b, c):
t = a ** 2 + b ** 2
if t == c ** 2:
return True
else:
return False

こんな感じでしょうか。OKって論理型引数が無いですけど、通常そんなものはいらないでしょう。そして、実際True/Falseと言う論理値が「返り値」になってるんで、実質上、これは(工学的定義に於いての)関数です。
なお、通大の「疑似プログラム例」に合わせて適合するように上のコードを記述しましたが、論理値を返すだけ、だったら次のようにもっと短く記述する事が可能です。


# -*- coding: utf-8 -*-

def Pythagorean(a, b, c):
return a ** 2 + b ** 2 == c ** 2

これが成り立つのは、そもそも論理演算子(ここでは==)が元々論理値(True/False)を返すから、です。
少なくとも引数が整数の限り、上の関数で十分目的は果たしています(通大のテキストが言う通り、一般に引数が「実数」だったら成り立ちませんが)。

Rubyでの手続き(メソッド)の例


既に解説した通り、純粋な意味では通大テキストでの「手続き」の例は破綻しています。
取り合えず、3平方の「疑似プログラム」に従って「真偽値を返す」メソッドをRubyで記述してみましょう。


def Pythagorean a, b, c
t = a ** 2 + b ** 2
if t == c ** 2
return true
else
return false
end
end

こんな感じでしょうか。OKって論理型引数が無いですけど、通常そんなものはいらないでしょう。そして、実際true/falseと言う論理値が「返り値」になってるんで、実質上、これは(工学的定義に於いての)関数です。
なお、通大の「疑似プログラム例」に合わせて適合するように上のコードを記述しましたが、論理値を返すだけ、だったら次のようにもっと短く記述する事が可能です。


def Pythagorean a, b, c
return a ** 2 + b ** 2 == c ** 2
end

これが成り立つのは、そもそも論理演算子(ここでは==)が元々論理値(true/false)を返すから、です。
少なくとも引数が整数の限り、上の関数で十分目的は果たしています(通大のテキストが言う通り、一般に引数が「実数」だったら成り立ちませんが)。

Schemeでの手続きの例


既に解説した通り、純粋な意味では通大テキストでの「手続き」の例は破綻しています。
取り合えず、3平方の「疑似プログラム」に従って「真偽値を返す」手続きをSchemeで記述してみましょう。


(define (Pythagorean a b c)
(let ((t (+ (expt a 2) (expt b 2))))
(if (= t (expt c 2))
#t
#f)))

こんな感じでしょうか。OKって論理型引数が無いですけど、通常そんなものはいらないでしょう。そして、実際#t/#fと言う論理値が「返り値」になってるんで、実質上、これは(工学的定義に於いての)関数です。
なお、通大の「疑似プログラム例」に合わせて適合するように上のコードを記述しましたが、論理値を返すだけ、だったら次のようにもっと短く記述する事が可能です。


(define (Pythagorean a b c)
(= (+ (expt a 2) (expt b 2)) (expt c 2)))

これが成り立つのは、そもそも論理演算子(ここでは=)が元々論理値(#t/#f)を返すから、です。
少なくとも引数が整数の限り、上の関数で十分目的は果たしています(通大のテキストが言う通り、一般に引数が「実数」だったら成り立ちませんが)。

Rでの手続き(関数)の例


既に解説した通り、純粋な意味では通大テキストでの「手続き」の例は破綻しています。
取り合えず、3平方の「疑似プログラム」に従って「真偽値を返す」関数をRで記述してみましょう。


Pythagorean <- function(a, b, c) {
t = a ^ 2 + b ^ 2
if (t == c ^ 2) {
return(TRUE)
}
else {
return(FALSE) } }

こんな感じでしょうか。OKって論理型引数が無いですけど、通常そんなものはいらないでしょう。そして、実際TRUE/FALSEと言う論理値が「返り値」になってるんで、実質上、これは(工学的定義に於いての)関数です。
なお、通大の「疑似プログラム例」に合わせて適合するように上のコードを記述しましたが、論理値を返すだけ、だったら次のようにもっと短く記述する事が可能です。


Pythagorean <- function(a, b, c) {
return(a ^ 2 + b ^ 2 == c ^ 2) }

これが成り立つのは、そもそも論理演算子(ここでは==)が元々論理値(TRUE/FALSE)を返すから、です。
少なくとも引数が整数の限り、上の関数で十分目的は果たしています(通大のテキストが言う通り、一般に引数が「実数」だったら成り立ちませんが)。


いままでは、関数や手続きを実際にプログラミングするときに必要となる知識を含めて説明してきました。ここで一番大事なのは、その関数はいかなる値をかえすのか、または、その手続きはいかなる仕事をするのかということです。すなわち、サブプログラムの仕様を正確に把握することです。詳細な実行部分を書くのが後になる場合でも、それを完全に理解していなければ関数や手続きを使う意味がありません。

<練習問題>
2次方程式の係数が与えられたときの判別式を計算する関数を書き、仮引き数を明示しなさい。


<回答例>


# -*- coding: utf-8 -*-
# Pythonの場合

def Discriminant(a, b, c) :
return b ** 2 - 4 * a * c

# Rubyの場合

def Discriminant a, b, c
return b ** 2 - 4 * a * c
end

;; Schemeの場合

(define (Discriminant a b c)
(- (expt b 2) (* 4 a c)))

## Rの場合

Discriminant <- function(a, b, c) {
return(b ^ 2 - 4 * a * c)
}

制御の基本3構造

さて、厳密には2〜4のどれか、は問いませんが(笑)、ここから制御の基本構造の解説に入っていきます。
もう一度繰り返しますが、通大の「コンピュータ」のテキストの最大のミスは


このテキストローカルの「疑似コード」で抽象的にプログラミング言語を解説しようとしている

この一点に尽きます。
しかも、その疑似コードの実態は「Pascalそのものだ」と言うことも既に指摘していて、いわゆる通常の意味での疑似コードそのものの「良さ」は丸っきり失われている、んです。
そんなまだるっこしい事をするんだったら「Pascalを」素直に使った方が良い、のです。


注:ちなみに、元々Pascalは「教育用プログラミング言語」として開発された経緯があります。従って、理想的には「一番最初に学ぶべき」プログラミング言語として設計されてはいるんですが、残念ながらそんなに人気が出ませんでした。実用的な分野ではC言語に負けてしまったのです。
理由として、「教育用を念頭に入れていた為」、構文が厳密過ぎて自由度が低い事がプログラマに嫌われた、って事が一つ(その点、Cの方が意外にハッカー好みのデタラメさがあったりする・笑)。もう一つは、C言語は元々UNIXと言うOS設計用言語として開発された経緯があったため、大学/研究所内のUNIXワークステーションの普及に従ってC言語は「MUST言語」になった、と言うのがPascalの不幸でした。
一方、Pascalは初期のApple Macintoshと言うパソコンのソフトウェア開発用言語として使われていた経緯もありますが、Macintoshそのものが価格が高すぎて普及せず、結局、認知度が低いプログラミング言語になってしまったのです。


今まで「プログラムの基本要素」「基本データ型」全て見てきました。が、退屈でしょう(笑)?ツッコミ入れてるこちらも退屈でしゃーない(笑)。
何せ、抽象的な「疑似コード」を用いて「抽象的に解説してる」んだから当然、です。実際「目の前にある何か」を動かして反応見ているわけでもない。こう言う形の「プログラミング言語の学習」ってのは通常「あり得ない」んですよ。
やっぱり目の前に「動いている何か」があるに越したことないです。

さて、プログラミング言語毎に結構バラけている「基本データ型」と違い(もちろん、共通するものも多いですが、違いも結構あります)、制御構造自体は「2〜4のどれか」はともかくとして(笑)、「基本」と言うだけあって、共通点が多いです。
よって、このテキストに準拠する以上、実際のプログラミング言語を登場させてみる、のはこの節が一番適切でしょう。
ではどのプログラミング言語を用いるか?
まず、ハッキリ言うと、テキスト準拠を考えると、通大「公式指定」の4つのプログラミング言語(Fortran、BASIC、Pascal、C言語)はどれもサイテーです(笑)。少なくとも前2者は選ぶべきじゃない(1960年代以前の遺産、です)。そしてPascalのコンパイラは見つかりづらいし、C言語はコンパイラは見つかりやすいんですが、初心者向けじゃないです。
そこで代替案に4つの言語を紹介しておきましょう。

最初に断っておきますが、通大の「レポートが通る」とか「テストが通る」とかそう言う低レベルの話をしてるつもりは丸っきりありません。どっちかと言うと、こう言う経験を元に「一生(何らかの)プログラミング言語と付き合って使っていける」と言うのを目標とします。そう言う大きい目標がある以上、「レポートが通る」とか「テストが通る」なんてのは小さい話なんでどーでもいい、です。
少なくとも、今まで高校数学の窓で見かけた通大生の投稿を見る限り、「目の前のレポート/テストさえ通れば良い」と言う態度がありありで、そんなんだったらそもそも教師なんて辞めた方が良い、です。大体生徒にプログラミング訊かれても答えられないですしね。
いずれにせよ、通大の「コンピュータ」のテキストが古いわ、ある意味システム的に破綻してるわ、だと一見回り道に見えても「別言語を経由した後」対策を考えた方が結果早い、って事です(これは通大数学コースのプログラミング/SE経験者の動向見ても予測可能でしょう)。

ここで紹介する4つの言語はどれも「オープンソース」で「マルチプラットフォーム」で「タダで入手可能」でしかも「高機能」です。

一般に、最低でも 3 種類のベンダーの OS でサポートされていない言語は、どれでもハッキング(注:良いプログラミングの事)にはまずい言語といえます。


もう一つ重要なのは基本的に「インタプリタ」、つまり対話型でのプログラミングが可能なプログラミング言語、を選びました。
もちろん、この4つ全部を覚えろ、ってな気は毛頭ありません。このうちのどれか一つを選んで、そこそこ使えるようになれば、逆にFortranだろうがBASICだろうがPascalだろうがC言語だろうが「何となく」読めるようにはなる、んです(そもそも通大のテキストの本質的なネタは"数値計算"ですから、そんなに言語自体に深い知識が必要ではないのです)。
ただし、要するに「最初の一歩」として、どの言語を選ぶか?と言うのは割に「個人的な好みの問題」ってのがあるんですよ。平たく言うと「見た目」だとか(笑)。そんなわけで万全を期して「4つ」としました。
もう一つの目的としては、「コードの構成法」と言うのは使うキーワード類は違うとしても、大まかに言うと「同じだ」からです。最初期の状態から自分で使わないにせよ、「他の言語の書き方」に目を通しておけば、ここで紹介した言語「以外」に移った時に、あまりビビらなくて済む(笑)。要するに「プログラミング言語に対する勘/慣れ」を早めに増加させる目的もあります。
いずれにせよ、一部の通大生(だと思いたいんですが)は「情報を入手」しても「実行しない」らしい、って事は言えます。ここで情報を入手して「やってみる」と思えるか、あるいは「メンド臭い」と思うか。いつぞや書きましたが「メンド臭い」と思う人はそもそも理系に向いてないんで、数学教師になろう、なんて大それた事は考えない方が無難です。

なお、4つのプログラミング言語の紹介にはサンプルコードを付しておきます。このサンプルコードは通大「コンピュータ」のテキストp.140のPascalでの「Summation」のコードをそれぞれの言語に置き換えてみたもの、です。
本当に1対1対応で、それほど各言語の特徴が出ていませんが、いずれにせよPascal版より「短くなってる」と言う事だけは分かると思います(少なくとも、変数宣言が無い分だけは短くなります)。そして、結果サンプルコードは「どの言語にせよ、同じ結果を返す」と言う事が保証されているので(元ネタのコードが同じ為)、後は「見た目」で好みのプログラミング言語が選びやすくなっているでしょう。

まずは最初に「動く疑似コード」と世界的に評価が高い言語二つを紹介します。




Python



1991年にオランダ人Guido van Rossumによって開発された比較的新しいプログラミング言語。元々はPascal同様に教育目的に開発されて習得しやすい、と言われている。21世紀のBASIC、と呼んでも良い言語である。世界的に評価が高く、人気があるプログラミング言語である。また、GoogleをはじめとしたIT企業でも開発に用いられている程、である。日本公式サイトはPyJUG

コード例



# -*- coding: utf-8 -*-

def Summation():

n = input(u"自然数nを入力してください: n = ")

if n <= 0:
raise "ValueError"
else:
sum = 0
for k in range(1, n + 1):
sum += k

print u"1から%dまでの和 = %d" % (n, sum)




特徴



  • まさしく「動く疑似コード」と呼ぶに相応しく、ロジックに集中して非常に短くコードを書き上げる事が可能。

  • この短さ、の秘訣のうちの一つは、構文要素にインデント(字下げ)が含まれており、ブロック構造を全てインデントで表現する、と言う特徴があるから、である(何らかのキーワードなり記号が必要ない)。

  • 従って、現代的なインデント中心のソースコードの「読み書き」に一番習熟出来るので、「教育用言語」の面目躍如である。

  • 基本的にC言語で実装されている(Java版もアリ)。


長所



  • ダウンロードすると、オールインワンの開発環境が手に入り、余計なものが必要ない。

  • C言語的な記述法の影響を受けていて、C言語に移りやすい。


短所



  • 現時点では、日本語の扱いが若干弱い。

  • 当面の問題としては、数学的計算をさせる際、ライブラリを明示的にインポートする必要があり、また、計算速度もそれ程速くない。


メーリングリスト


何かPythonで分からない事があったら次のメーリングリストで誰かが親切に答えてくれるかもしれません。

Python MailingList Japan

推薦コメント



もしコンピュータ言語をなにも知らないなら、まず Python から始めることをおすすめします。設計がきれいだし、ドキュメントもしっかりしているし、初心者にもそこそことっつきやすくできています。でも入門言語として最適でも、おもちゃではありません。強力で柔軟で、大きなプロジェクトにもじゅうぶん対応しています。


総評


実は、高校数学の窓でどのプログラミング言語を紹介しようか、と迷って、最後に残った二つの候補のうちの一つがこのPythonだったんです。残念ながら恒常的な数値計算ネタには向かないだろう、ってんで結果Schemeを選んだ、のですが、一般的な意味では「一番書きやすく」かつ「見た目が良い」のはPythonだと思います。つまり、ソースコードの可読性を考えるとピカイチだと言う事です。
ここで、「Pythonが良いな」と思った人はすぐさま次のダウンロードページに進んでPythonをダウンロードして下さい。

Python標準リリース

また、オープンソースらしく、オンラインドキュメントが豊富なのがPythonの魅力です。が、通大のテキストを理解する程度なら、次の二つのオンラインドキュメントをやる程度で十分でしょう。


また、若干古いのですが、次のような入門サイトもあります。

取り合えず、暫くは(と言うか、恐らく最初の二つの文書を勉強するとしても1〜2週間もあれば充分でしょう)上の文書類に取り組んで、それからここへ戻って来てください。では、Python組は、行ってらっしゃい(笑)。





Ruby



1993年に日本人まつもとゆきひろ氏によって開発されたプログラミング言語。元々UNIX用スクリプト言語(簡易言語の一種)として開発された為、UNIXサーバーとの相性が抜群で、Webアプリケーションの開発にも向いている。日本国内で絶大な人気を誇り、また世界的な評価もうなぎ上りの注目株である。公式サイトはRuby


コード例



def Summation

print("自然数nを入力してください : n = ")
n = gets.to_i

if n <= 0
raise
else
sum = 0
for k in 1..n
sum += k
end

puts '1から' + n.to_s + ' までの和 = ' + sum.to_s
end

end


特徴



  • 基本的にC言語で実装されている(Java版もアリ)。



長所



  • 日本で設計された為、日本語を含む多言語の扱いが比較的容易である。

  • あらゆる形であらゆる言語の「長所」をなるべく取り入れるように設計されている為、かなり短くコードを記述出来る。

  • また、基本的な数学的な機能は全てビルトインなので、余計なライブラリを呼び出す必要があまりない。

  • 日本生まれのお陰で日本人のユーザー数が多く、結果として日本語で読めるWeb情報が極めて多い。



短所



  • 元々、「プロ用」に設計されている為、自分で開発環境を用意しなければならない。

  • インタプリタ機能(REPL = Read Eval Print Loop)でさえ、自分で設定する必要がある。

  • ブロック構造の終端をendを置いて明示的に示さなければならない為、タイプ量が比較的多くなり、気づいてみれば記述したソースはendだらけになってたりする。

  • 計算速度は大して速くない。



メーリングリスト


何かRubyで分からない事があったら次のメーリングリストで誰かが親切に答えてくれるかもしれません。

メーリングリスト


Web雑誌


昔のパソコンのプログラミング雑誌(例えばベーマガとか)よろしく、日本Rubyの会ではWeb雑誌も発行しています。
当然無料なんで、こう言う雑誌を購読しても良いでしょう。

Rubyist Magazine


推薦コメント


教育用の言語としてRubyは最適です。


総評


日本生まれの日本が世界に誇るべきプログラミング言語、です。日本製のプログラミング言語でここまで世界で人気を博したものはかつて存在しませんでした。
サンプルコードを見ても分かると思いますが、殆どPythonと変わりません(endがある分だけちょっとだけPascalっぽいです)。
実際、「使われ方」はPythonと競合してて、Pythonの最大のライバル、です。世界的情勢では今の時点ではPythonの方が人気がありますが、今後数年間で立場が逆転する可能性さえあります。
Webでの使用例も多く、気づかないかもしれませんが「Rubyで書かれたWebサイト」なんかも知らず知らず使っている可能性さえあります。
武田先生もRubyを学んでいた事がある、とかどっかに書いてたと思うんで(当然、僕より遥にRubyに詳しいと思います)、レポート問題の丸投げやるんだったら「Rubyで書いてみて」「質問とソースを投稿して」「あとで十進BASICになり何なり自分で訳してみる」んだったら武田先生も回答してくれる可能性があるでしょう(と言うか、「最後は自分で訳してみる」くらいアタマ使え、って事です)。
ここで、「Rubyが良いな」と思った人はすぐさま次のダウンロードページに進んでRubyをダウンロードして下さい。

ダウンロード

また、Rubyの場合、WebチュートリアルがPythonのものと比べても「非常に上出来」だと思います。非常に分かりやすい「プログラミング入門」が公開されていて、初心者向けですね。これを終わらせれば通大の「コンピュータ」のテキストを読解するのも簡単だろうと思います。
また、Rubyの難点、つまり「開発環境選択」とか「インタプリタの設定」なんかもこのチュートリアルに記載されているんで、「言われた通りに設定していけば」何も問題は生じないと思います。

プログラミング入門 - Rubyを使って -

ちなみに、上記のチュートリアル(日本語版)は、千葉大学の工学部のプログラミング未経験者の為の初級者向けテキスト、として実際使われていた実績があるようです。
分量もそんなに多くありませんので、一日一章進めていっても2週間以内で終わる計算となります。
取り合えず、暫くは上の文書に取り組んで、それからここへ戻って来てください。では、Ruby組は、行ってらっしゃい(笑)。


注:他にもWindows向けのRubyの開発環境設定には以下のような方法がある。

  • Rubyそのもののインストール法→Rubyの準備を参照。写真入りで解説されていて、比較的初心者にも分かりやすい記述である。

  • Windowsユーザー用Ruby開発環境→RDEを参照。原始的なテキストエディタであるWindows備え付けのメモ帳(Notepad)でもプログラムを書けなくはないが、色々とメンド臭い。そこで「Rubyでプログラムを書く為の」色々な機能が付いた統合開発環境(IDE=Integrated Development Environment)があると便利。RDEはRubyに特化したIDEである。亀田は未使用だが、プログラミング初心者にもお薦めならしい。





これら二つの言語(PythonとRuby)は「動く疑似コード」と呼ばれているだけあって、「ロジックに集中して書く」事が可能です。と言うか「ロジックだけ」書けば良い。工学的な「余計な要素」が基本的に存在しません。
通大のワケの分からない「疑似プログラム」と違って、通常「疑似コード」と言うのは、繰り返しますが、「汎用的なロジックだけを抜き出して書く」のが特徴なのです。従って、これらの言語の人気は「わざわざ疑似コードを紙の上で書くくらいだったら初めからPythonやRubyで記述してしまった方が早い」と言う事実に裏付けられています。実際、これらで一旦「目の前で動かしてみて」速度的に問題が生じた場合、C言語等で「部分的に書き直してみる」と言うのがプロの現場でも良くある、と言う話です。通大のワケの分からない「疑似プログラム」を使って書いて考えるよりも「実践的だ」と言う事ですね。

残り二つの言語は既にここで紹介して使っているもの、です。が、改めて紹介しておきましょう。




Scheme



1975年にMIT(マサチューセッツ工科大学)ガイ・スティールとジェラルド・ジェイ・サスマンによって開発された関数型プログラミング言語。Lisp方言の一つである。元々は理論的実験の為に作成されたが、教育用プログラミング言語として改良され、MIT(マサチューセッツ工科大学)をはじめとした世界中の様々な大学の情報工学系の授業で用いられている。日本での主な使用大学は広島大学東京工業大学東京大学等。


コード例



(define (Summation)
(display "自然数nを入力してください : n = ")
(let ((n (read)))
(if (<= n 0)
(error n)
(do ((k 0 (+ k 1))
(sum 0 (+ sum k)))
((> k n)
(for-each display
(list
"1から " n "までの和 = " sum "\n")
)
)
)
)
)
)



特徴



  • 元々、Schemeの元ネタ、Lisp自体が工学出自の「設計の為に設計された」プログラミング言語ではなく、数理論理学者であるアロンゾ・チャーチ等によって提唱された数学理論、ラムダ算法を元とした純粋な数学から来ている。

  • SchemeはそのLisp言語族の中でも一番シンプルで小さい方言である。

  • 従って、「計算モデル」を論じるのには一番適切で、また、数学を「計算」に置き換えるのも一番面倒が少ない。

  • Python、Rubyのように、基本的に「単一の公式実装しか存在しない」わけではなく、Schemeはキチンと公式に文書として定義されているプログラミング言語である。実装数はこの世にフリーのものから商用のものまで合わせて10個以上存在していて、好みの実装が選べる。

  • 故に「公式な仕様書が存在し」「複数ベンダーによる複数の実装が存在している」以上、C言語やPascal等と同等な、「メジャー言語」である(情報工学系に於いての認知度、と限定はされるが)。

  • 実は実装の存在数自体はひょっとしてC言語やPascalよりも多いかもしれない。これも「メジャー性」を裏付けている。

  • お薦めがオールインワンの実装、DrSchemeだが、要するに別実装もたくさんあるので、その辺は好みで良い。

  • なお、日本では、プロ用目的で作られ、Webアプリケーション作成で定評のある、日本製の実装Gaucheが特に人気がある(がWindowsでは基本的には動かない)。Windows用にはGaucheboxと呼ばれるオールインワンの開発環境が用意されている。



利点



  • 構文らしい構文が一つしかない。

  • その唯一の構文がS式と呼ばれる(手続き 引数)と記述する表記法である。SchemeでのプログラミングはそのS式を入れ子状に引数内に記述していくだけ、であり、原理的にはMicrosoft Excelの関数と大して変わらない単純なものである。

  • 従って、自由度が高く、割に「デタラメに書いても」平気で動いてくれる。

  • また、数学出自らしく、ビルトインの数学関数も豊富で、また論理構造の一貫性も高い。

  • 実装にもよるが、現代のPCの環境では、基本的に計算は高速である。



欠点



  • とにかく括弧だらけで、一般的感覚で言うと「読みづらい」。

  • また、「一貫性が高い」のトレードオフが前置記法(ポーランド記法)と言う数理論理学出自の数式記述法で、慣れないとちょっとした数式さえも書きづらい。



推薦コメント



LISP は、それをモノにしたときのすばらしい悟り体験のために勉強しましょう。この体験は、その後の人生でよりよいプログラマーとなる手助けとなるはずです。たとえ、実際には LISP そのものをあまり使わなくても。



Cがコンピュータがどう動作するかのモデル化に最も適した言語とすると、Lispは計算というものがどう振る舞うかをモデル化するのに最も適した言語だ。ほんとのことを言って、Lispについて多くのことを知る必要はない。一番シンプルでクリーンなSchemeを使い続けることだ。他の Lisp方言はライブラリやらツールやらを備え、C++やJavaが持つような大きく複雑なプログラミング環境へと成長している。そういう部分については知っている必要はない。ただSchemeでプログラムを書ける必要がある。




僕は、Schemeの開発こそ、コンピュータ・サイエンスの典型的なセンスの一つではないかと思うのである。それまでは、LISPの考案者McCarthyのまやかしともいえる「変数の動的なスコープ」に、皆がだまされ続けていたわけである。それを、「静的スコープ」でLISPが作れるということを身を持って証明したのが、Schemeの開発であった。Schemeの開発は様々な要素から成り立っている。まず、静的スコープの方がλ算術の理論との整合性がよいという認識がある。そのような認識を得るには、もちろん、λ算術を知らなくてはならないけれども、それほど数学的なことまで知る必要はない。また、いくら理論との整合性がよくても効率よく実現できなければどうしようもないから、静的スコープを効率よく実現するための技術が必要となる。さらに、いくら理論との整合性がよく実現の効率がよいとしても、プログラミング言語として、わかりやすく使い易いものでなければならない。そのためには、シンタックスを工夫したり、環境を整えたりしなくてはならない。以上のような様々な問題を克服して初めて、静的スコープが現実のものとなり、Schemeが本物のプログラミング言語となったのである。



総評


Lispそのものに付いては大した解説はいらないでしょう。通大の「コンピュータ」のテキストにも解説は書いてありますし。p.20辺りに次の記述があります。

1958年にマサチューセッツ工科大学(MIT)ジョン・マッカーシーJohn McCarthyを中心とした人工知能研究グループによって考案されたプログラミング言語で、この分野の研究には欠かせない言語です。LISPはLISt Processorの略と言われています。
LISPは言語名の由来からも分かるように、リスト(節点と枝を持ち、情報と情報間の関連の両方を表すデータ)を主なデータ構造とする言語で、節点と枝に適当な意味を持たせることにより広範囲の情報をコンピュータ処理の対象にできるのが特徴です。
応用分野には図形処理、数式処理、自然言語処理、知的ゲーム、知的な推論を必要とする質問システム(エキスパートシステム)等があります。これらの分野では、必ずしも常に成立するとは限らないが、多くの場合に問題解決に有効な働きをする経験的知識を使ってプログラムの実効効率を上げたり、その結果を見てさらにプログラムの改良したりすることが良く行われます。このようなプログラミングの方法を発見的プログラミングheuristic programingといいます。これに対して、常に成り立つ知識を使って問題解決の手順を組み立てていくことをアルゴリズミック・プログラミングalgorithmic programmingといいます。

もはや後半になると何の事を言ってるのかサッパリですが(笑)、まあ、Schemeはこう言ったLisp言語族の一つ、です。
Lisp言語族は恐らく「史上最強のプログラミング言語」で、前出のPython、Ruby等もこの「Lisp言語族」の影響を受けています(特にRubyがその気が強いですし、作者もそう語っている程、です)。
欠点としては、既に挙げましたが、「前置記法」と「括弧の多さ」ですね。それに加えて「数学ベース」なんで、「非常に強力な数学的記述法」を用いる代わりに、「数学的過ぎる」辺りが一般的に人気が出ない理由でもあります。ただし、「数学的」を嫌うのは「数学教員育成コースの学生」としてはおかしな話です。自ら「数学が出来ない」って言ってるようなものですから。

ちなみに、面白い現象なんですが、大学の情報工学関係出身者でこの「数学性」でLispのファンになる人間は多いんですが、反面、理学部数学科出身者の方は「Lispを知らない」と言うケースが結構多い、ってのにビックリします。
大学内は縦割りなんで「学部をまたいで情報が広がっていかない」のか、あるいはやっぱり「餅は餅屋」なのか。逆にBASICは「数学者が直接作った(かつ設計がマズい言語)」なんで数学関連で用いられるケースはいまだあるようですが、もったいない話、です。

実際、ちょっと慣れるまで時間がかかるのは事実ですが、反面、「括弧の多さ」と言うのは慣れればさほど気にならなくなります。
大体、Schemeに慣れてくると、上のサンプルコードは「脳内変換」されて次のように読まれてたりするのです。

define (Summation)
display "自然数nを入力してください : n = "
let n (read)
if (<= n 0)
error n
do (k 0 (+ k 1)
(sum 0 (+ sum k)))
(> k n)
for-each display
list
"1から " n "までの和 = " sum "\n"


括弧外してみると、実は形式的にはPythonやRubyのサンプルコードと大して変わんない事に気づくでしょう。例外は「反復」であるdo〜以下でこれは確かに見づらいと僕も思うんですが、概してSchemeユーザーはdoなんて使わないんで構わないのです。じゃあ、何でここで使ったのか、と訊かれるでしょうが、単に元ネタのPascalのコードと対比させる為、だけです。通常こんな書き方はしません。
いずれにせよ、括弧は大した問題じゃなく、慣れれば「消えて見える」ようになります。

弟子が尋ねた。「先生、私は先生がカッコをまるで魔術師のように扱っているのを常々敬服しています。どうすれば先生のようになれるのでしょうか?」
師「えっ? カッコ? あ、そうか。そんなものもあったな。いやあ、 すっかり忘れておったわ」



とにかく、Schemeは「括弧の帳尻合わせで」ソースコードを書いたり読んだりして行くのではなくって、あくまで「インデント」(字下げ)で読み書きしていくのです。
構文上は自由度が高いSchemeですが、括弧の数の多さにより、逆説的に「Pythonのように」インデントが極めて重要な言語、となっています。インデントが無ければ「書くのも」「読むのも」非常に大変になる、と言う事を表しているのです。
また、通大のテキストの記述によると「Lispは人工知能向け」と言う事が仄めかされているので、

「人工知能、って難しい分野でしょ?だったらSchemeも難しいんでしょ?」

と不安がる向きもあるでしょうが、全然関係ないです。と言うのも「人工知能」は難しい研究分野ですが、イコールSchemeが難しい、ではない、と言う事です。
何故なら、「難しい研究を難しい道具でやる」ってのはバカのする事です。正解は「難しい研究は簡単な道具でやる」です。研究分野が難しいわ、使う道具も難しいわ、じゃ普通に考えると行き詰まっちゃうんですよ(笑)。従って「難しい研究分野こそ」簡単な道具を使わないとやってられない、んです。BASICは「簡単だ」ってイメージがありますが(イメージだけ、です)、実際「人工知能」でBASICが使われている、なんて話は聞かないでしょう?見た目はともかくとして、BASICより簡単な言語がLisp言語族なのです。
ここで、「Schemeが良いな」と思った人はすぐさま次のダウンロードサイトに進んで次のソフトウェアをダウンロードして下さい。

魔法言語リリカル☆Lisp

これはScheme実装ではないのですが、チュートリアルとしてはピカイチ、です。全12章のアドヴェンチャーゲームソフト、として作成されているので遊びながらSchemeの基礎が学べます。これも2週間程度で終わらせる事が出来るでしょう。京都工芸繊維大学の学生さんの作品です。
注意点としては、テキストエディタが必要となります。そのうちC言語を使うにせよ、「しっかりとした」テキストエディタがどの道要り用になるんで、ここでは魔法言語リリカル☆Lispとリンクが出来るxyzzyを推薦しておきます。xyzzyはWindows用の超高機能なフリーのテキストエディタ、です。
xyzzy用のプラグインは以下から入手して下さい。

xyzzy lyrical-mode

xyzzy lyrical-modeを解凍すると、中にlyrical.lと言うファイルがあるんで、そこに記述された説明に従って設定して下さい。
取り合えず、魔法言語リリカル☆Lispに取り組んで、それからここへ戻って来てください。
では、Scheme組は、魔法言語リリカル☆Lispで遊んでらっしゃい(笑)。




R



1993年頃、ニュージーランドのオークランド大学のRoss IhakaとRobert Gentlemanにより作られた。豊富な統計解析用関数をビルトインで持つ統計解析用プログラミング言語である。ただし、両2名が言語自体をデザインしたわけではなく、これは元々米国AT&Tで開発された商用の統計解析用言語Sのフルスクラッチのコピー版である。


コード例



Summation <- function() {

n <- readline("自然数nを入力してください : n = ")

if (n <= 0) {
return (NA)
} else {
sum <- 0
for (k in 1:n) {
sum <- sum + k
}

cat("1から ", n, " までの和 = ", sum, "\n")
}
}




特徴



  • ここに紹介した4つのプログラミング言語の中では、制御構造の記述法が一番C言語のそれに近い。

  • その大きな理由は、元々Sを開発した米国AT&TがUNIXとC言語を開発した会社だから、である。

  • UNIXの端末による対話型環境が探索型統計解析と相性が良い為、Sは元々UNIX用の統計解析用途のプログラミング言語として設計された。

  • Sはアメリカ計算機学会の1998年度ソフトウェアシステム賞を受賞している。

  • Rは文法的にはそのSのクローンだが、内部的にはSchemeの影響を濃く受けている。

  • 従って、「Cの皮を被ったScheme」である。

  • SもRも現在では世界中の統計解析専門分野、例えば医療統計の分野、経済予測の分野、等で幅広く用いられている。日本の大学での文系の統計ではSPSSの独断場だが、理系ではコスト面からバッチ式で商用のSASよりRが使われるケースが徐々ではあるが増えている。



利点



  • 統計解析用言語、と銘打つだけあって、疑似乱数の精度が高く、他のプログラミング言語のビルトイン乱数の追従を許さない。→メルセンヌ・ツイスタ法



欠点



  • 汎用プログラミング言語ではない為、ファイルの読み込みや綺麗な表形式のデータの書き出し、表示には強いが、一方、一般的なプログラミング言語では当たり前の単純な入出力が意外と弱かったりする。



総評


この中では異端の言語だとは思います。統計解析用ツールと言えば良いのか、プログラミング言語と言えば良いのか……。
しかしながら、見方を変えてみると、プログラミング言語は「プログラムを書く道具」なのはともかくとして、「それ自体がプログラム」でもあるわけです。この境界線は、GUIのアプリケーションとはイメージが違い、コマンドラインのアプリケーションでは極めて曖昧です。
また、Rでは制御構造の記述法が極めてC言語に近い事もあって、これを終えた後、C言語「には」移りやすいでしょう。この「C言語臭さ」が好みが分かれるところではありますが(ちなみに、亀田はこのC言語臭さが大っ嫌いです)。
一方、通大生は統計を苦手とする人も多いようで、どうせだったら「プログラミングの勉強+統計」を両方一緒に勉強する、と言うような一石二鳥を狙う人がいてもいいだろう、って事で、まあ、ある種「オマケ」ですが、Rに関しても紹介しておきました。
Webチュートリアルに関しては……難しいですね。恐らくR-Tipsが一番良いのではないでしょうか。ただし、これはここで紹介した他の言語と違い、1〜2週間で終われるような分量じゃない、と思います。
しかしながら、これをやってのけたら……まあ、通大の「コンピュータ」のテキストを読まずして通大の「コンピュータ」のテキストを全て理解してしまう可能性があります。と言うのもR-Tipsは統計関数の使い方は当たり前として、プログラミング入門、通大のテキストの肝である「数値計算法」の記述もあり、要するに「てんこ盛り」なのです。恐らく、通大の「統計」用の参考書+「コンピュータ」くらいの内容はあるでしょうね。
まあ、そんなわけで、ここでRを選んだR組は帰ってこなくてもいいかもしれません(笑)。ダウンロードや設定はR-Tipsに記述があるんで頑張ってください(笑)。

なお、ここではこれら4つの動的型付け言語を利用して通大の「コンピュータ」のテキストを読み解いていきますが、それでもFortran、BASIC、Pascal、C言語をやりたい、って人もいるでしょう(あるいは、レポート/テスト対策の為には明らかに必要でしょう)。そう言う人の為には最後にコンパイラ等の入手方法や参考書等を紹介した付録を付けておくので、「自習」して下さい。Pascalはともかくとして、Fortran、BASIC、C言語と言う「全く性質が違う」言語でこのテキストを解説するのは不可能だと思います(そして、Pascalは入手出来さえすれば、テキスト読解はそんなに難しく無いでしょう)。

では、4つの動的型付け言語を使ってテキストの読解に入ります。また、「まだ選べない」人にとっても、「制御構文の好み」でこれ以降の節ですんなりどの言語にしようか選べるとは思います。
本題です。


コンピュータは、プログラムの中で次のように書かれているとき、

  • 命令文1

  • 命令文2

  • 命令文3

  • ・・・

  • ・・・


上から順番に命令を実行していきます。この命令の逐次処理がコンピュータの基本です。このテキストでは、上のように1行に1命令を書くことを原則とし、それを上から順に逐次実行していくものとします。


これは本当に基本です。どんな言語でも、「上から順番に命令を実行していきます」。割に単純な事なんですが、ソースを読む場合やプログラムを組む場合、「これを忘れる」事がままあるワケです。
逆な言い方をすると、以降の分岐や反復の存在意義は、この「バカ正直に上から順番に命令を実行していく」コンピュータの処理の流れを若干変化させる為、だけにあります。だから「制御」なんですね。
簡単な例題を考えてみます。次の計算を順番通り行うとしましょう。

$1 + 2$……(答え)3
$2 \times 3$……(答え)6
$5 - 8$……(答え)-3
$9 \div 2$……(答え)4余り1

左がプログラムで右が計算結果だとして……仮に「上から順番に計算を実行しないで」右の答えが羅列されたらどうなるでしょう?「計算は正しい」のに、「順番がグチャグチャ」……。そんな事はあり得ませんね。と言うか、あっちゃ困る、のです。
つまり、計算の指示順と計算結果の表示順は「同じ並びじゃなきゃ困る」ってのが大原則なんです。これは上のような「単純計算」じゃなくても原則は同じ、です。それが計算機の使命、なんです。
バカバカしいかもしれませんが、上の例に従って、4つの言語での逐次処理プログラムの例を見てみます。




Pythonだとこう。




print 1 + 2
print 2 * 3
print 5 - 8
print 9 / 2






Rubyだとこう。




puts 1 + 2
puts 2 * 3
puts 5 - 8
puts 9 / 2






Schemeだとこう。





(+ 1 2)
(* 2 3)
(- 5 8)
(/ 9 2)







Rだとこう。





print(1 + 2)
print(2 * 3)
print(5 - 8)
print(9 / 2)




表示指定なんかが違ったり、特に割り算でどこまでの値を返すのか、言語によって違いはありますが、しかしながら、「上から順番に計算していって返す値も同じ順」だ、と言う事が分かるでしょう。
また、4つの言語のそれぞれのソースも基本的には全く同じで記法意外は殆ど変わりません。
しつこいようですが、これが「逐次実行」でプログラミングの基本中の基本です。

あらゆるプログラミング言語は上から順番に命令を実行していく

これを念仏のように唱えてください。これが分からないと、プログラミングではドツボなのです(しかも、アタマで分かっても「感覚的に」分からないケースが多い、のです)。
何かしら作成したプログラムが「思った通り動かなかった場合」、分岐/反復等の制御を疑うのは当然なんですが、分岐/反復は必ずその「ブロック」(実行範囲)を持ちます。そしてその「ブロック内」での記述に於いて「自分が企画した通りの順番に並べたのか?」。ブロック内に於いてもこの「逐次実行のルール」と言うのは厳然として存在するんで、そこでの「並び」、あるいは「命令の配置」は必ずチェックする必要がある、のです(プログラミング初心者はこの辺で必ずミスをする、んです)。


このような上から順番に命令を実行するだけで、最終の目的に達することができれば、それにこしたことはありません。しかし、現実にはコンピュータになんらかの状況判断をおこなわせて、それによって処理の流れを変えてやらなければなりません。そのためにあるのが、分岐処理です。
分岐の基本は、

  • もし  (命題)  ならば 命令文T
           さもなくば 命令文F


または、

  • if  (命題)  then 命令文T
            else 命令文F


と書かれるものです。このかたちの制御で、なんらかの判断をコンピュータに行わせることになります。これは、"もし"、"if"の後にある(命題)が真のとき、命令文Tを実行し、その(命題)が偽のとき、命令文Fを実行します。すなわち、(命題)の真偽によって2つの命令文のうちのどちらか一方のみを実行します。この後は、どちらの命令文を実行したにせよ、次に書かれた命令文を実行することに注意してください。この分岐の変形として、

  • もし  (命題)  ならば 命令文T


または、

  • if  (命題)  then 命令文T


と書かれる、(命題)が真のときのみ命令文Tを実行する制御の仕方もあります。
さて、ある命題が真のとき実行したい命令が1文で記述されるならば、上の形だけで十分ですが、いくつか命令を書きたいときはどうするのでしょうか。これは複文という考え方を用いて解決されます。

  • もし  (命題)  ならば {
           
    • 命令文T1

    • 命令文T2

    • ・・・

    • ・・・

    • 命令文Tx

                }
          さもなくば {
    • 命令文F1

    • 命令文F2

    • ・・・

    • ・・・

    • 命令文Fx

                }


と書いて、(命題)が真のとき"{"と"}"で囲まれた、命令文T1、命令文T2、・・・、命令文Txまでを順に実行し、偽のときは、やはり"{"と"}"で囲まれた、命令文F1、命令文F2、・・・、命令文Fxまでを順に実行します。ここで"{"と"}"で囲まれた一連の命令文を、一つの命令文であると解釈すれば、前の書き方と整合がとれます。これを複文といいます。

後半辺りにCが混じってきますね(笑)。"{"と"}"で囲んでブロックを表現するのは基本、Cから来ています。
ここで実際、4つの動的言語の分岐の記述法を上の例に倣って見てみましょう。

Pythonの分岐




if 命題 :
命令文T1
命令文T2
・・・
・・・
命令文Tx
else:
命令文F1
命令文F2
・・・
・・・
命令文Fx

非常にシンプルです。殆ど上で解説されている分岐と変わらないでしょう。
Pythonの場合、複文をまとめる「ブロック」は"{"と"}"で表現せずに、全てインデント(字下げ)で行います。行頭が揃っている部分が「ブロック内部の命令」と言う意味です。また、ifやelse等の分岐キーワードの最後には必ずコロン(:)を付けます。また、命題を"("と")"で囲む必要もありません。これがPythonの特徴です。
もっとも、インデントに関して言うと、Python備え付けの開発環境(IDE)がコロン(:)を入力した時点で「この後にはブロックが来る」と判定して勝手に字下げしてくれます。従って、そこまでプログラムを書く側が一々インデントを気にしなくても良いのですが、それでもインデントがどう付けられるか、注意して見てください。

Rubyの分岐




if 命題
命令文T1
命令文T2
・・・
・・・
命令文Tx
else
命令文F1
命令文F2
・・・
・・・
命令文Fx
end

Rubyも形式的にはPythonと殆ど変わりません。命題自体を"("と")"で囲む必要はありません。
Pythonとの違いのポイントとしては、

  1. if、等のキーワードによる構造を記述する際は、必ず最後はendで終わらないとならない。

  2. if、else等を記述した行にコロン(:)を書く必要が無い


と言う辺りでしょうか。

Schemeの分岐


Schemeの分岐はPythonとRubyに比べてちょっと変わっています。
と言うより、通大のテキストの説明には一番適合するやもしれません。



(if (命題)
(命令文T)
(命令文F))

これが分岐の基本形です。ただし、通大のテキストやPython、Rubyと違って、まずはelseが必要無い、です。(命題)が真であれば即座に命令文Tが実行されますし、(命題)が偽だったら命令文Fが即座に実行されます。そしてこれが意味するのは、理論的にはelse、ってのは要らないのです。
で、基本形はこのまま、なんですが、通大のテキストで言うところの「複文」を実行させる際には、別のキーワードが必要となります。



(if (命題)
(begin (命令文T1)
(命令文T2)
(・・・)
(・・・)
(命令文Tx))
(begin (命令文F1)
(命令文F2)
(・・・)
(・・・)
(命令文Fx)))

実は、Schemeではブロック内で逐次実行する場合は明示的にbeginと言う命令を使わなければならず、これが通大のテキストで言う"{"と"}"の役割を果たしています(と言うか、もっと本当の事を言うと、Schemeにはブロック、複文と言う概念すら存在しないんですが、今のところはそう言う解釈をしておいて構いません)。

Rの分岐





if (命題) {
命令文T1
命令文T2
・・・
・・・
命令文Tx
}
else {
命令文F1
命令文F2
・・・
・・・
命令文Fx
}

これが一番通大「コンピュータ」のテキストに書かれている構文例にクリソツですね。
(と言うより、C言語のものにクリソツなんですが)


コンピュータが得意とすることの1つに、繰り返しがあります。人間は単純な反復処理を嫌いますが、コンピュータは文句一ついわずに黙々と、何千回、何万回であろうと、いいと言われるまで繰り返してくれます。プログラムでは、これをうまく使わなくてはなりません。
基本となる反復処理は、その繰り返し回数がすでに分かっているときに使われる、次のようにかかれるものです。

  • iをmからnまでとして次を繰り返す
    {

    • 命令文1

    • 命令文2

    • ・・・

    • ・・・

    • 命令文x


    }


ここで、iは整数型変数、mとn(m≦n)は整数値(整数型変数を使うときもあります)です。これによって、まず変数iにまず変数mを代入して、それから"{"と"}"で囲まれた命令文を上から順に、命令文xまで実行します。次に変数iの値を1だけ増加して、同じ"{"と"}"で囲まれた命令文を実行します。これを、変数iの値がnになるまで繰り返し、変数iの値がnより大きくなれば、括弧内の命令文は実行されずに制御はこの後に書かれている命令文に移ります。すなわち、

  • i = m として{・・・}内を実行する

  • i = m + 1として{・・・}内を実行する

  • ・・・

  • ・・・

  • ・・・

  • i = n として{・・・}内を実行する


と記述したことと同じことです。結局"{"と"}"で囲まれた一連の命令文を(n - m + 1)回繰り返すことになります。この変数iを通常、ループの制御変数といいます。数学で添字といえば、文字i、jが一般にはよく用いられます。制御変数によって、何回目の繰り返しであるか分かります。その情報は、繰り返される処理の中でよく利用されます。


これは一般にforループ、と言われるものの説明です。



……。
え〜と、上の説明もベースはC言語のもの、ですね。"{"と"}"で囲まれる部分がforループのブロックでその中の命令が逐次処理で繰り返されます。
これも実際、4つの動的言語で見てみましょう。

Pythonのforループ




for i in range(m, n + 1):
命令文1
命令文2
・・・
・・・
命令文x

Pythonのforループは上の説明と一ヶ所だけ食い違っています。
それは、

変数iの値がnになるまで繰り返し

と言う部分で、rangeによる指定で「n回目まで」を目論んでいるんだったら「n + 1」と記述しなければならない、と言う事です。これはここでは端折りますがrange関数の性質に由来します。
その他の特徴としては、forと言うキーワードも「これからループ用のブロックが記述されますよ」と言うサインとして行末にコロン(:)が必要な事、そしてそのブロック内の命令群はforの位置より「一段深く」インデントを噛まさなければならない、と言う事です(これもIDLEが勝手にやってくれますが)。
従って、

変数iの値がnより大きくなれば、括弧内の命令文は実行されずに制御はこの後に書かれている命令文に移ります。

の「この後に書かれている命令文」はforの位置と行頭が揃ってなければなりません。
具体的に見ると次のような事です。


for i in range(m, n + 1):
命令文1
命令文2
・・・
・・・
命令文x
命令文y # ここはforループ外

上の例で見ると命令文yはforループブロック内のインデントと行頭が揃ってません。従って、これはPythonにより「ループ内命令」とは解釈されずに、「forループが終わった後に」実行される命令となります。
なお、Pythonでは#から行末までは「コメント」として解釈されて、実行には関係が無くなります。書いたコードにメモを記入しておく為にこの「コメント」を積極的に利用しましょう。このような「コメント」が可読性を上げる鍵になります。

Rubyのforループ




for i in m..n
命令文1
命令文2
・・・
・・・
命令文x
end

Rubyのforループの方が素直かもしれません。これは通大のテキストの通り、の動作をします。また、forループの終わりもendで明示的に閉じなければならない、と言うのも分岐の書き方と同じです。
このように、Rubyは大変整合性が取れています。
なお、RubyのコメントもPythonと同じ作法で、#から行末まではコメントとしてRubyに無視されるようになっています。

Schemeのforループ


Schemeにはforループはありません。
なお、Schemeのコメントはセミコロン(;)から行末まで、となっています。

Rのforループ





for (i in m:n) {
命令文1
命令文2
・・・
・・・
命令文x
}

形式的には通大のテキストの記述に一番一致しそうです。ただし、RのforループはC言語のそれ、とも違います。恐らく事実上、この構文はRubyのものに一番近いでしょう。
なお、RのコメントもPython、Rubyと同様、#から行末までインタプリタに無視される形となっています。


繰り返しの回数が前もって分からない場合は、反復を終了するためのなんらかの判断を必要とします。分岐処理と同様に、条件式等で記述される命題を用いて、次のように表現することにします。

  • (命題)である限り次を繰り返す
    {

    • 命令文1

    • 命令文2

    • ・・・

    • ・・・

    • 命令文x


    }



これにより、まず(命題)が真か偽を調べて、真であれば"{"と"}"で囲まれた一連の命令文を実行します。命令文xの実行が終了すると、次に、また(命題)が真か偽か調べ、真であれば括弧内の命令文を実行します。命題が偽になった時点で制御は、この後に書かれている命令文に移ります。
このかたちの反復は、(命題)が最初から、偽であれば括弧内の命令文は一度も実行されません。また、(命題)が常に真であれば、永久的に括弧内の命令を順に実行するだけで、あとの命令文が存在したとしても、それが実行されることはありません。このことをよく無限ループに陥るといいます。


これは一般にwhileループ、と言われるものの説明です。
これも通大のテキストでは形式をC言語から借りてきています、が、割にwhileは分かりやすく、似たような構文を持っている言語が多いようです。
では見てみましょう。

Pythonのwhileループ




while 命題:
命令文1
命令文2
・・・
・・・
命令文x

これもPythonらしく、インデントの法則は一致しています。whileブロック内のインデントは一段深くなり、whileの行末はコロン(:)が必ず要り用です。
また、whileを抜けた後の命令文のインデントは一段浅くなるのもforループと同様です。


while 命題:
命令文1
命令文2
・・・
・・・
命令文x
命令文y # ここはwhileループ外


Rubyのwhileループ




while 命題
命令文1
命令文2
・・・
・・・
命令文x
end

これも形式的には通大のテキストやPythonと同じですね。Rubyらしくwhileブロックの後には明示的にendで閉じる必要があります。

Schemeのwhileループ


Schemeには公式にはwhileループはありません(持っている実装もありますが、非公式なんでここでは取り上げません)。

Rのwhileループ





while (命題) {
命令文1
命令文2
・・・
・・・
命令文x
}

これも形式的には通大のテキスト、そして構文的にはC言語そっくりです。


1番目の反復のかたちは、実は2番目の反復のかたちを用いて表現することができます。すなわち、

  • i←m

  • (i≦n)である限り次をくりかえす
    {

    • 命令文1

    • 命令文2

    • ・・・

    • ・・・

    • 命令文x

    • i←i + 1


    }


と書けば、これは1番目の反復処理とまったく同じことを、コンピュータに指示したことになります。
反復処理のなかに一度無条件で一連の命令文を実行した後で、それらをある条件が満足されるまでさらに繰り返すものがあります。ここではその形の反復処理の説明は省略することにします。


これは良くある練習問題だと思います。forループをwhileループに書き直したり、whileループをforループに書き直したり。
まあ、その辺は各自自習してみてください。そして、これは「パズル」と言うより、問題を解くにあたって、どっちかが「より短くコードを記述出来る」場合があるから、なんです。だから殆どのプログラミング言語に最低でもforループとwhileループの二つが搭載されているのです(ただし、Schemeは除きますが)。要するに一つは「適切な問題に適切なループを使おう」と言う練習なのです。
もう一つ理論的な意味もあって、多くの言語では、実はforループかwhileループのどちらかを「基本」として持っています(ただしやっぱりSchemeは除きます)。そして、プログラミング言語の内部で、実際にforループをwhileループに「変換」したり、その逆をやってたりするわけです。「基本」を持ってて、理論的には「必要の無い」もう片方のループを実装する。これは単純にプログラムをする側の利便性の為、です。こう言う「本質的には必要が無いんだけど、敢えて利便性の為だけに組み込まれた(しかし実際は内部的には変換される)」構文を専門用語でシンタックス・シュガー(構文糖衣)と呼びます。そのシンタックス・シュガーを実感する練習でもあるわけ、ですね。

ところで、Schemeにはforループもwhileループも存在しません。代わりに上の「変換の約束事」に良く似たdoループ、と言われるものを持っています。
ここではそれを軽く紹介しておきましょう。

Schemeのdoループ





(do ((i m (+ i 1))) ;最初にiにmを代入してiを1づつ増やす
((< i n) (命令文y)) ;iがnを越えたらループを脱出する
(命令文1) ;ループ内ではこれら命令を逐次実行
(命令文2)
(・・・)
(・・・)
(命令文x))

これを見ると、上のforループ←→whileループ間の変換の要素が全て入ってるのが分かると思います。
が、鬼のような読みづらさ、です。そして、実際、Schemeユーザーはこのdoループが大っ嫌いで、殆ど使いません。しかもこれもシンタックス・シュガーなのです。
では、forループもwhileループも無しでSchemeではどうやってループを実現しているのか?
その秘密はおいおい分かってくるでしょう。そして結論から言えば、ここで紹介した4つの動的言語は全てループ構文無しでループを書けちゃうのです。
従って、ここで解説したループに関する通大テキストの読解は、ある意味全て無意味になります(笑)。

いままで見てきた逐次、分岐、反復を使って命令文の実行の順序を制御します。コンピュータは、まったく融通がききませんから命令文の順序を一つ間違えただけでも正しい結果を得ることはできません。プログラムを作成するとき、手順、段取りの順序を、一分の隙もなく、正確に指定しなければなりません。

<演習問題>
100点満点として、

  • A 80以上

  • B 70以上80未満

  • C 60以上70未満

  • D 60未満


のように、成績をつける処理の流れを記述しなさい。



Pythonでの回答例


一つのやり方としては、今までの制御構造の流れを用いて次のように記述する方法である。


if x <= 80:
print "A"
else:
if x <= 70:
print "B"
else:
if x <= 60:
print "C"
else:
print "D"

このように、ifelseの構文は入れ子(ネスト)に出来、インデントを読んでいくと「どんどん深くなっていく」のが分かるだろう。これが一つの解答法、である。
しかし、一般的にプログラマはあまりにも深くネストされたプログラムを好まない。出来れば同じような制御は同じようなインデントレベルに揃えたい。これも可動性を高める為、である。
こう言う場合は、Pythonではifelifelseと言う形式を用いる。


if x <= 80:
print "A"
elif x <= 70:
print "B"
elif x <= 60:
print "C"
else:
print "D"

これならインデントのネストはそんなに深くならない。
なお、問題文には「◯◯以上、××未満」と言う条件が付加されているのに、ソースコードでは「未満」は反映されていない。何故その必要が無いのか、と言うのも「逐次実行」の性質による。
例えば、点数が75点だったとして、当然一番最初にチェックされるのは「80点以上かどうか」である。ここで「偽」を喰らうと次の時点で「70点以上かどうか」をチェックされるわけだが、その時は既に「最初のテストをクリアしてない」ので80点未満なのは自明となる。従って、こう言う「上から順に実行される」コンピュータの性質では命題をパスしたかしないか、で後続の処理が施される為、それ以上考える必要が無い場合があるのだ。
これが逐次実行の美味しい性質なのである。

Rubyでの回答例


一つのやり方としては、今までの制御構造の流れを用いて次のように記述する方法である。


if x <= 80
puts "A"
else
if x <= 70
puts "B"
else
if x <= 60
puts "C"
else
puts "D"
end
end
end

このように、ifelseの構文は入れ子(ネスト)に出来、インデントに従って読んでいくと「どんどん深くなっていく」のが分かるだろう。これが一つの解答法、である。
しかし、一般的にプログラマはあまりにも深くネストされたプログラムを好まない。出来れば同じような制御は同じような字下げのレベルに揃えたい。これも可動性を高める為、である。
こう言う場合は、Rubyではifelsifelseと言う形式を用いる。


if x <= 80
puts "A"
elsif x <= 70
puts "B"
elsif x <= 60
puts "C"
else
puts "D"
end

これならインデントのネストはそんなに深くならないし、特にRubyの場合、ifelseの制御で一つendが必要になるので、最初のコードだと最後がendだらけになってしまってたが、ifelsifelseだと一つの制御構造なので、endは一つだけ、に抑えられる。
なお、問題文には「◯◯以上、××未満」と言う条件が付加されているのに、ソースコードでは「未満」は反映されていない。何故その必要が無いのか、と言うのも「逐次実行」の性質による。
例えば、点数が75点だったとして、当然一番最初にチェックされるのは「80点以上かどうか」である。ここで「偽」を喰らうと次の時点で「70点以上かどうか」をチェックされるわけだが、その時は既に「最初のテストをクリアしてない」ので80点未満なのは自明となる。従って、こう言う「上から順に実行される」コンピュータの性質では命題をパスしたかしないか、で後続の処理が施される為、それ以上考える必要が無い場合があるのだ。
これが逐次実行の美味しい性質なのである。

Schemeでの回答例


一つのやり方としては、今までの制御構造の流れを用いて次のように記述する方法である。



(if (<= x 80)
'A
(if (<= x 70)
'B
(if (<= x 60)
'C
'D)))

このように、Schemeの構文は入れ子(ネスト)に出来、インデントを読んでいくと「どんどん深くなっていく」のが分かるだろう。これが一つの解答法、である。
Schemeは「入れ子で書く」プログラミング言語ではあるが、出来ればインデントレベルは揃えたい。これも可動性を高める為、である。またifififも数が多すぎて目障りである。
こう言う場合は、Schemeではcondelseと言う形式を用いる。



(cond ((<= x 80) 'A)
((<= x 70) 'B)
((<= x 60) 'C)
(else 'D))

これならインデントも深くならないし、見た目もカッキリとしてシンプルである。
なお、問題文には「◯◯以上、××未満」と言う条件が付加されているのに、ソースコードでは「未満」は反映されていない。何故その必要が無いのか、と言うのも「逐次実行」の性質による。
例えば、点数が75点だったとして、当然一番最初にチェックされるのは「80点以上かどうか」である。ここで「偽」を喰らうと次の時点で「70点以上かどうか」をチェックされるわけだが、その時は既に「最初のテストをクリアしてない」ので80点未満なのは自明となる。従って、こう言う「上から順に実行される」コンピュータの性質では命題をパスしたかしないか、で後続の処理が施される為、それ以上考える必要が無い場合があるのだ。
これが逐次実行の美味しい性質なのである。

Rでの回答例


一つのやり方としては、今までの制御構造の流れを用いて次のように記述する方法である。



if (x <= 80) {
print("A")
}
else {
if (x <= 70) {
print("B")
}
else {
if (x <= 60) {
print("C")
}
else {
print("D")
}
}
}

このように、ifelseの構文は入れ子(ネスト)に出来、インデントを読んでいくと「どんどん深くなっていく」のが分かるだろう。これが一つの解答法、である。
しかし、一般的にプログラマはあまりにも深くネストされたインデントを好まない。出来れば同じような制御は同じようなインデントレベルに揃えたい。これも可動性を高める為、である。
こう言う場合は、Rではifelse ifelseと言う形式を用いる。
これもC言語の形式に準じている。



if (x <= 80) {
print("A")
}
else if (x <= 70) {
print("B")
}
else if (x <= 60) {
print("C")
}
else {
print ("D")
}

これならインデントのネストはそんなに深くならない。
なお、問題文には「◯◯以上、××未満」と言う条件が付加されているのに、ソースコードでは「未満」は反映されていない。何故その必要が無いのか、と言うのも「逐次実行」の性質による。
例えば、点数が75点だったとして、当然一番最初にチェックされるのは「80点以上かどうか」である。ここで「偽」を喰らうと次の時点で「70点以上かどうか」をチェックされるわけだが、その時は既に「最初のテストをクリアしてない」ので80点未満なのは自明となる。従って、こう言う「上から順に実行される」コンピュータの性質では命題をパスしたかしないか、で後続の処理が施される為、それ以上考える必要が無い場合があるのだ。
これが逐次実行の美味しい性質なのである。