高校数学の窓過去問検索

質問<3700>2008/3/30 from=nanaya 「BASICプログラム」

次の問題をBASICプログラムで書きたいです。
問)10進法で表された数を2進法、16進法の表現に変換するプログラム
を書きなさい。
2進法に変換するプログラムは作れたのですが、16進法に変換するプログ
ラムの書き方が分かりません。よろしくお願いします

★希望★完全解答★





2進法に変換するプログラムは作れた


2進法に変換するのも16進法に変換するのも、基数さえ取り替えればアルゴリズムは全く同じです。
故に、2進法に変換できれば16進法にすぐ変換できます。
割る側の数値を2から16へと取り替えて下さい。




では、いつも通りBASICの問題をSchemeで解題してみましょう。

とは言っても、この問題の場合、実は驚くほど単純です。
従って、解説も単調にならざるを得ないのですが、ここでは

プログラムはどうやって作るのか?

と言う実践中心で解説してみたいと思います。
と言うのも、実際問題、一発で狙ったプログラムが書けるのはほぼ稀なんですよ。
従って、「何度も動かしてみてはおかしい部分の手直しをしていく」ってのが実際的な作業となるんです。
通常、プログラムの入門書なんかではいきなり「ベストな解法」が書かれていたりするんで、

「こんな風にすぐ書けるなんて凄いな〜〜。」

なんて勘違いするんですが、実はそんな事はほぼあり得ません(笑)。本当は「キレイな解」に到達するには背後にすっごい量の試行錯誤がある、って事なんです。
プログラム初心者の人は「取りあえず動くモノに出来た」ってだけで、結構ヘトヘトになるんですが(経験済み・笑)、もちろん実際問題、「プログラムを書く」ってのは数学の知識はあった方が良いのですが、どっちかと言うと「小説を書いたり」「詩を書いたり」するのと同じなんですね。「ザーっと書いてみては読み返して修正していく」と言う作業が実は大半を占めるのです。
今回はこの「修正」に焦点を当ててやっていってみましょう。

ところで、10進法からn進法への変換アルゴリズムですが、一応知らない人の為に軽くアルゴリズムを紹介しておきましょう。
これは実は驚くほど簡単です。
なお、与えられる数値mもnもここでは整数とします。


  1. 整数mをnで割り、商をx、余りをyとする。

  2. 余りyが一桁目の数となる。

  3. 商xをnで割り、その商をz、余りをwとする。

  4. 余りwが二桁目の数となる。

  5. 商zをnで割り……と商が0になるまで同じ作業を繰り返す。

  6. 得られる数は××××wy、と言う形になる。



簡単でしょ?
まだピンと来ない人もいるとは思うんで、具体的に見てみますか。
例えば整数m=37として、二進数に変換したい、とする。
ここでn=2ですね。
上で解説された作業に従うと、


  1. 整数37を2で割ると、商が18、余りが1となる。

  2. 余り1が一桁目の数となる。

  3. 18を2で割ると、その商は9、余りは0となる。

  4. 余り0が二桁目の数となる。

  5. 9を2で割り、その商は4、余りは1となる。

  6. 余り1が三桁目の数となる。

  7. 4を2で割ると、その商は2、余りは0となる。

  8. 余り0が四桁目の数となる。

  9. 2を2で割ると、その商は1、余りは0となる。

  10. 余り0が五桁目の数となる。

  11. 1を2で割ると、その商は0、余りは1となる。

  12. 余り1が六桁目の数となる。

  13. 商が0になったので計算は終了。求める2進数は100101となる。



とまあ、解が求まるワケです。簡単ですね。
しかも16進数の場合は2で割らずに単に16を使えば良い、と言う事です。
ところが、人手で計算するのはメンド臭い。そこでコンピュータの出番となるワケです。かいつまんで言うとプログラムを書く、と。

さて、上記のアルゴリズムを問題に沿ってSchemeで実装すると、プロトタイプは以下のようになります。



;;十進法から二進法への変換プログラム

(define (decimal->binary m)
(let loop ((i m) (ls ()))
(if (zero? i)
ls
(loop (quotient i 2) (cons (modulo i 2) ls)))))


;;十進法から十六進法への変換プログラム

(define (decimal->hexadecimal m)
(let loop ((i m) (ls ()))
(if (zero? i)
ls
(loop (quotient i 16) (cons (modulo i 16) ls)))))



いつも通り名前付きlet構文を用いた再帰的定義に依る繰り返し計算でプログラムを記述しています。局所関数loopを中で定義して、それの引数を順次更新する形、ですね。
まあ、この辺までは「慣れてくると」ササ、っと書けてきます。
なお、ちょっとだけ解説をしておきます。

  • decimal…英語で10進法の事。これは定義した関数の名前の一部に使われている。

  • binary…英語で2進法の事。bi-は2を表す接頭語。語源はギリシャ語と言うお話。

  • hexa-…英語で6を表す接頭語。これも語源はギリシャ語らしい。従ってhexa+decimalで「16進法」となる。

    註:一般的にラテン語系は形容詞が後ろから被さる、と言う構造が多いようだ。従って、hexa+decimalは60ではなく16となるらしい。面白いね(笑)。

    なお、「ヘキサ」と言うとアレを思い出すでしょうが、その通りです(笑)。















    ♪好きになったね ヘ〜キ〜サ〜ゴ〜ン〜〜〜

    と言うアレです。



    ホントどうでもイイ話ですが(爆)。

  • Schemeは通常のプログラミング言語と違い、除算を実行すると分数で解を返してしまいます。従って


    (/ a b)


    と言う形式はこの問題の場合適しません。
    そこで、商を求める組み込み関数quotientと、余りを返す関数moduloを併用して、局所関数loopの引数を書き換えています。


さて、前述の2つの関数、decimal->binaryとdecimal->hexadecimalは、商と余りを求める計算に用いてる2と16が違うだけであとは全く同じです。
そうすると、事実上「全く同じプログラムを二つ書いてしまった」と言う事です。
まあ、2つ程度同じプログラムを書いても大した事は無いんですが、今後「10進法を8進法に変換しなさい」なんて言う問題を見かけたらどうしましょう?「また同じプログラムを書かなきゃならない」と言うことになりかねません。これは避けたい。
ここでプログラムを書く人は思うワケです。

「どうせアルゴリズムは同じなんだし、今後再利用する事も考えて、2進法だろうが8進法だろうが16進法だろうが計算してくれるプログラムを書いた方が便利なんじゃない?うん、そうしよう。」

これがコードの再利用性です。2度と同じ問題を解かなくていいようにしよう、と思うワケです。
そこで、前出の二つのプログラムを一つにまとめてみよう、とします。具体的には商と余りを計算する為の数、2と16をもっと抽象的な文字nで置き換えてしまう。これは数学で良く使われる抽象技法ですね。
そして、そのnも外部から与える引数、として設定してしまう。これが最初の書き換え、です。
以上のアイディアの元で書き換えたプログラムは以下の通りになります。




;;十進法をn進法へ変換するプログラムver.1

(define (decimal->npn m n) ;引数にnを追加
(let loop ((i m) (ls ()))
(if (zero? i)
ls
(loop (quotient i n) (cons (modulo i n) ls))))) ;nで商と余りを計算



なお、npnはN positional notation(N進法)の略、です。まあ、この辺は名前なんで適当に名づければいいのですが。
取りあえず、上手く動くかどうか、インタプリタで実行結果を見てみます。



はい、どうやら上手く行ったようですね。上の写真のように


(decimal->npn <計算対象の数> <基数指定>)


と指定すれば2進法であろうと16進法であろうと計算してくれるようです。
が。しかし。

「計算突っ込む為のスペースにリスト(配列)使っちゃったから、表示がリストになっちゃってるや。一応もうちょっと数字らしく表示させてみようかな?」

と思いつく。「計算過程」では便利なんで用いたリスト(配列)なんですが、出力には少々ふさわしくない、と判断します。
また「手直し」ですね。
ここで、リストをそのまま数字として表示させるには


(for-each display <リスト>)


と言う形式を用います。
実はこのfor-eachと言うのは以前やったmapと言う高階関数の仲間なんですが、引数として取る関数が「計算を全く行わない」性質の関数の時用います。具体的にはこの例ではdisplayですね。
ちょっと細かい話なんですが、実はプログラミング言語の理論では、「表示」とか「ファイルの読み込み」とかその手の入出力関係は「計算を全く行っていない」事になっているんです。例えば3と言う数字を画面に出力するにせよ、3自体が「何か計算されて変化する」ワケではありません。従って、理論上では「何も行っていない」と言う事になってしまうんです(ウソ!!!って思うかもしれませんが、本当です・笑)。
こう言う「計算に全く関係ない作業」を特にプログラミング言語では副作用と呼んだりする事があります。Schemeでは出力表示命令の「display」なんてのは「副作用」の代表格なのです。
まあ、この辺の理論的背景はともかくとして、取りあえずリストを受けてリストを無視して出力させたい場合は、上記の


(for-each display <リスト>)


と言うフォームで書けば良い、って事だけ分かれば今のトコ充分です。
さて、ではどこで上のフォームを被せるのか?と言う話なんですが、単に商が0だった場合のリストが出力結果だったんで、そこに被せれば良いだけだ、ってのは自明ですね。
やってみましょう。



;;十進法をn進法へ変換するプログラムver.2


(define (decimal->npn m n)
(let loop ((i m) (ls ()))
(if (zero? i)
(for-each display ls) ;ここに被せる
(loop (quotient i n) (cons (modulo i n) ls)))))



では実験してみましょう。



お。何か上手い具合に動いてますね。しめしめ。
ところで、一つ気になることがあります。
例えば10進法で16と言う数は16進法では10になる、って事は分かるでしょう。まあ、当たり前ですよね。16で始めて一桁繰り上がる、ってのが16進法の定義ですから。
問題は、では仮に10進法での15は16進法ではどうなるんでしょうか?上で作ったプログラムで計算してみましょう。



あれえっ!!??と俺は椅子から3センチ程飛び上がった。おかしいなあ。このプログラムはどっか間違いがあるぞう(筒井康隆調)。
いや、実はロジックは全然間違っていないのです。
実際、上のプログラムで10進法の16を入力して計算させてみると、次のような結果になります。



つまり、16は15より当然大きいワケですし、その関係は10進法が16進法になっても変わりません。が、上の出力結果を見る限り、10は15より大きい、と言うような結果に見えます。
実はこれは勘違いで、どうして10進法の15を16進数で表すと15が出てくるのか、と言うのは単に

普段我々が使用している10進法での数値表記では16進法の一桁の数字すべてを表すことが不可能だから

と言う事なんです。表記用の数字が6個足りないのです。つまり、プログラムのアルゴリズムの問題じゃなくって、単に「表記法の問題」なんですね。
まあ、ある意味、そこが不具合なんですけど、アルゴリズムのロジック自体が間違ってるワケではない、と言うことに要注意です。


「う〜〜ん、そうかあ……。んじゃあ取りあえず、10進法の10を16進法ではAにして10進法の11は16進法ではBにして……(以下省略)。条件節を上手く使えば何とかなるかな?」


と考えます。
さて、どこに条件節をブチ込みましょうか?
元々、10進法からn進法に変換された数値データを保有しているリストはVer.2の5行目の第二引数が鍵を握っているのです。


(cons (modulo i n) ls)


ここで、(modulo i n)により算出された「余り」をデータ保持の為のリストlsに順次突っ込んで行ってる。
つまり、(modulo i n)の計算結果を直接追加していくのではなくって、ここに条件節を上手く適用すれば狙った結果になるんじゃないか、と予測出来るワケです。
そうすると、局所変数letを上手く使って、変換されるべき数値/文字データを上手く嵌めてみたいと思います。雛形は以下の通り。



(let ((x (modulo i n))) ;(modulo i n)をxに代入
(cond ((= x 10) 'A) ;xが10の時Aを返す
((= x 11) 'B) ;xが11の時Bを返す
((= x 12) 'C) ;xが12の時Cを返す
((= x 13) 'D) ;xが13の時Dを返す
((= x 14) 'E) ;xが14の時Eを返す
((= x 15) 'F) ;xが15の時Fを返す
(else x))) ;それ以外の時はxのまま



と言うことは上と先ほどの構造の中の(modulo i n)を単純に取り替えれば良いワケです。



(cons (let ((x (modulo i n)))
(cond ((= x 10) 'A)
((= x 11) 'B)
((= x 12) 'C)
((= x 13) 'D)
((= x 14) 'E)
((= x 15) 'F)
(else x))) ls)



そうすると、全体のソースコードは以下のようになります。



;;十進法をn進法へ変換するプログラムver.3

(define (decimal->npn m n)
(let loop ((i m) (ls ()))
(if (zero? i)
(for-each display ls)
(loop (quotient i n) (cons (let ((x (modulo i n)))
(cond ((= x 10) 'A)
((= x 11) 'B)
((= x 12) 'C)
((= x 13) 'D)
((= x 14) 'E)
((= x 15) 'F)
(else x))) ls)))))




ではまた実験してみましょう。



おお上手く行ってますね。
が。しかし。


「上手く動いてるけど、cond節(条件節)がウザいなあ……。7個も条件列挙するのはさすがに汚いよ。
どうせならデータベースか何かがあって、"勝手に変換"してくれりゃあイイのに。
大体これじゃあ16進法までしか計算できないしなあ。」



「ある程度上手く動く」事を確認したら意外と欲が出るモノです。「もっとキレイに、もっと機能的に」と言う欲が、ですね。
さて、先ほどのプログラムver3は「条件節」を使ってどの数値がどの文字に変換されるか明示的に指示していました。
が、別な方法としては、データ変換の為の約束事を別にまとめておいて、ある数がそのデータ構造を「参照」して、そこから結果を「引っ張ってくる」と言う手も考えられます。
この方式を提供する方法論はプログラミング言語によって違いますが、基本的には「配列」を用いて実現します。BASICで言うDATA文、C言語で言う「構造体」等、色々な形式があってそれを利用するのが一つの手です。
ここではSchemeのデータ型の一つ、ベクトルを用いてみます。

Schemeでのベクトルは概念的には数学で言うベクトルと全く同じである、と考えて間違いないです。
ただし、「実用的な性質」、つまりプログラミング言語を利用する上での特性、ってのが同時に絡んでくるんですね。

まずは書式から。基本的にSchemeで言うリスト(配列)とベクトルは構成も書き方も良く似ています。
例えば、リストは基本的に次のように書きます。


(a b c d)


一方、表記法としては、ベクトルはリストの先頭に#(シャープ)を付けただけのモノです。


#(a b c d)



特に違わない。
しかしこの2つは機能的には異なった部分があります。
かいつまんで言うと、リストは中に含まれる「要素」にアクセスする場合、原理的には「リストの先頭から要素を順番に探していく」と言う性質があります。
一方、ベクトルの場合は「要素番号を指定して」その要素へといきなりアクセスする事が可能なんです。
大して差が分からないかもしれませんが、要するに「いきなり目的の要素にアクセス出来る」と言う意味ではベクトルの方がスピードが速い、と言う特性があるんです。指定された要素以外には見向きもしません。
例えば、具体的には、上のリストとベクトルからcと言う要素を返したい場合、リストの場合、


(car (cdr (cdr '(a b c d))))
=>c


としないと目的の要素を取り出せません(これは原理的にはプログラミング言語の内部でも同じです)。動作の数が多いですね。
一方、ベクトルの場合、


(vector-ref #(a b c d) 2)
=> c


と目的の要素を直接指定して「そのまま」取り出す事が出来ます(なお、refは参照=referenceの事です)。
と、ここで。

「あれ?cってベクトルの3番目の要素でしょ?何で2、って指定してんの?」

と疑問に持った貴方は偉い(笑)。実は殆どのプログラミング言語ではそうなんですが、「順番は0番目から数え始める」と言うのが通例になっているんです。従って、ベクトルも、上の場合ではaは「1番目の要素」じゃなくって「0番目の要素」なのです。0、1、2、3、……と数えるのです。
さて、今ここでプログラム内にデータベースを突っ込むとして、仮に次の形のデータ構造をベクトルを使って定義した、とします。



#(0 1 2 3 4 5 6 7 8 9
A B C D E F G
H I J K L M N
O P Q R S T U
V W X Y Z)



上記のベクトルの0番目の要素が0、1番目の要素が1、…10番目の要素はA、…15番目の要素はF、…と先ほど考えた構造と親和性がありそうな気がします。非常にスッキリしている。
そうすれば、局所変数letと上記のベクトル表記を使って、参照先のデータ構造、tableを定義出来そうです。



(let ((table #(0 1 2 3 4 5 6 7 8 9
A B C D E F G
H I J K L M N
O P Q R S T U
V W X Y Z))))




そうすれば、tableのx番目の要素を取り出すためには、



(vector-ref table x)



とすれば良い。
さて、ではxが一体何になるのか?と言うと、これが先ほど出た(modulo i n)の計算結果です。これがデータベクトルtableの要素番号を指し示している、のです。
従って、わざわざxを使わなくても、直接(modulo i n)でxを置き換えれば良い。


(vector-ref table (modulo i n))


で、上式が何を返すか、と言うと当然tableに含まれている「指定された」要素です。
つまり、これらが計算結果として欲しいワケなんで、先ほど条件節で書き換えた再帰呼び出しされた関数loopの第二引数は、cond節を用いなくてもまるっきり上の式で代用できる、って事になります。


(cons (vector-ref table (modulo i n)) ls)


もう一度言いますが、vector-refでtableベクトル内の(modulo i n)番目の要素はそのまま計算結果を保持する為のリストlsに順次追加されていきます。これで構造は完璧です。
問題は、データベクトルtableをどこに置くか、って事ですが、プログラミング言語には大体のトコ次の2つの法則があります。


  1. 処理は上から下へと順に流れていく。

  2. 参照先のデータ構造は、大体のトコ計算本体の前に置けば良い。



先ほどのデータ構造tableは、上の法則を満たすように、計算本体、つまり局所関数loopが始まる前に置けば良い、と言う事です。特に、Schemeの場合は「括弧」があるんで、局所関数loopをtableを定義している局所変数letの括弧で「包み込むように」置くのがコツです。
上の2法則を満たすようにすると、結果、コードは次のようになります。



;;十進法をn進法へ変換するプログラムver.4

(define (decimal->npn m n)
(let ((table #(0 1 2 3 4 5 6 7 8 9 ;ここにデータベクトルtableを置く
A B C D E F G
H I J K L M N
O P Q R S T U
V W X Y Z)))
(let loop ((i m) (ls ()))
(if (zero? i)
(for-each display ls)
(loop (quotient i n)
(cons (vector-ref table (modulo i n)) ls))))))
;↑ここの部分を再び修正




さて、先ほど挙げた2つの法則は特にSchemeの場合は強力で、この2つを成り立たせるルールを特に字句的有効範囲、とか性的静的有効範囲とか、もっとダイレクトに英語でレキシカル・スコープ(Lexical Scope)等と呼びます。変数がどこまでの範囲で別の変数を参照出来るのか、それを決めるルールなんですが、名前は厳ついんですが、反面意味的には非常に分かりやすいルールです。コンピュータの専門の人は色々理屈を並べたがるんですが(笑)、「人間にとっても非常に分かりやすい」ルールになっています(と言うか、その為のルールなんですけどね)。

レキシカル・スコープとは、一般的なたとえ話をすると次のような意味です。
次の2つの文章を見比べて見てください。

  1. 桃太郎は桃から生まれました。彼は鬼ヶ島に鬼退治へ行きました。

  2. 彼は桃から生まれました。桃太郎は鬼ヶ島に鬼退治へ行きました。


プログラミング言語に於ける「変数」と言うのは、自然言語の文章で言うと、殆ど「主語」('何が'が指し示す部分)みたいなモノなんですが、当然「変数の受け渡し」と言うのは「代名詞が何を指してるのか」と共通している問題があります。
さて、上記の2つの文を見比べてみると、1番目の文章は、2個目の文章の主語である「彼」が1個目の文章の主語である「桃太郎」を指しているのが直感的に分かります。何故なら先に「桃太郎」と言う「主語」(プログラミング言語で言う変数みたいなモノ)が定義されているから、ですよね。これは文章の流れとしては非常に分かりやすい。
反面、2番目の文章は難解です。平たく言うと、最初に「彼」と言う代名詞が現れ、そして2番目の文章に「桃太郎」が現れる。果たして「彼=桃太郎」なのか、厳密に言うと「分からない」のです。
もちろん、僕等は人間ですから、色々な知識がありますし、推測する事は出来るんですが、反面、コンピュータは「結構バカ」です。推測なんて絶対にやりません。
従って、プログラミング言語を設計するに辺り、1番目の文章のように

誰が読んでも必要なモノは真っ先に定義されていて、文脈の順序が自然言語と比べてもナチュラルな変数の参照方法

を特に「レキシカル・スコープ」と名づけているのです。


註1:そして、名づけているには当然理由があって、それは明示的に一々「どの桃太郎が」とハッキリ指定しなければならないプログラミング言語もある、って事なのです。
註2:余談ですが、実はこの「誰が」と言うのを明示的に表現しない文章の書き方も当然あって、それが特に有効なテクニックとして使われているのが、推理小説の一手法だったりします。
推理小説の中には「殺人シーン」が描かれているんですが、代名詞だけでボカしたりして、具体的な「誰が」に付いて全く触れてない、と言う形式のモノがあります。当然、その「誰が」は推理小説の最後に明かされて、「あの人が!!!意外だ!!!」となるワケです(笑)。


上の「桃太郎文」の場合は、時系列(つまり、文には順序がある)が前提なんですが、Schemeの場合は特に、「括弧」があるんで、変数参照はより分かりやすくなっています。つまり、「括弧の内側にある変数(table)」から「括弧の外側にある変数(table)」は参照出来るんですが、その逆は出来ない、と言う事です。そう言う理由で、計算本体である「局所関数loop(tableと言う名前を中に持っている)」を、tableを定義している局所変数letで「包んで」いるワケです。
まあ、この辺の話は比較的「難しい範囲」だと思われる事もあるんで、完全に理解出来なくても構いませんが、取りあえず現代の比較的「新しい」言語はこのレキシカル・スコープを採用している場合が結構多いので、単語だけでも覚えておいたら得するかもしれません(笑)。また、どの道、先に挙げた2法則はワリにどんな言語でも通用する法則なんで、変数参照で困った時は変数の位置関係(登場順序)をチェックする習慣を付けると役に立つと思います。

さて、そう言った話はともかく、取りあえず作ったプログラムを実験してみましょうか。



上手く動いてますね。


「うっひょーッ!!!取りあえず今のデータ変換だと36進法まではオッケーだな。しめしめ。」


いわゆる10進法の表記文字0〜9の10個とアルファベットの大文字26文字合わせて36個です。取りあえず、当初の問題(10進数の2進数と16進数"だけへの"変換)はどこへやら(笑)。36進数までの表現を手に入れる事が出来ました。


「これでデータ変換の為の文字を追加さえすれば何百進法だろうと可能だけど……まあ、ここで止めておくか。36進法以上を指定すれば一応エラーを返すようにしておこう。」

よくよく考えてみれば他にも次の2つの暗黙の条件があります。

  1. 1進法以下のルールは無い。

  2. プログラムdecimal->npnが取る2つの引数m、nは両方とも整数じゃなければならない。


これも考えてみれば条件節を追加すれば何とかなるような気がします。
ここでは先に条件節を追加した「完成版」のコードを見てみます。



;;十進法をn進法へ変換するプログラム最終型

(define (decimal->npn m n)
(let ((table #(0 1 2 3 4 5 6 7 8 9
A B C D E F G
H I J K L M N
O P Q R S T U
V W X Y Z)))
(cond ((not (<= 2 n (vector-length table))) ;第二引数の範囲指定
(error "第二引数が範囲外です!:" n)) ;例外処理
((and (integer? m) (integer? n)) ;mとnの型を制限
(let loop ((i m) (ls ())) ;計算本体
(if (zero? i)
(for-each display ls)
(loop (quotient i n)
(cons (vector-ref table (modulo i n)) ls)))))
(else (error "引数は共に整数じゃないといけません!:" m n)))))
;↑例外処理



なお、新しく追加した命令「error」ですが、これはプログラムから「警告文」を発生させる為の処理です。
まあ、文脈的には元々、別に単なるdisplayでも構わないんでしょうが(※)、最近流行りの新しいプログラミング言語では「警告文表示の為の」特殊な命令を持つケースが増えてきています。こう言うのを例外処理と言うのですね。
1番目の条件は第二引数nが2以上データベクトルの長さ(vector-length table)以下かどうかチェックしています。もし、その範囲外にnがはみ出ていたら「第二引数が範囲外です!」と言う表示と共に具体的に問題があった入力nを返します。
なお、範囲の上限に(vector-length table)を使っている理由は、今後tableのデータを追加した場合、その長さが変化する可能性があるから、ですね。明示的に36、と数値を与えておくより安全だ、と言う事です(データを追加したりしても、そこの数値を変更するのを忘れる可能性がある)。
2番目の条件は第一引数mも第二引数nも共に整数であるかどうかチェックしています(integer?)。両方の条件をクリアしたら、ここで始めて局所関数loopが起動して「目的の計算」を遂行してくれます。ここがこのプログラムの本体ですね。
残りのケースは第一引数mか第二引数nのどちらかが整数ではない、と言う事です。ここでも計算上問題が生じます。従って例外処理をerrorで発動させて「引数は共に整数じゃないといけません!」と言うメッセージと共に問題がある可能性がある外部からの入力引数、mとnを返します。

※:少々複雑な話になるが、元々C言語はUNIXと言うOSを書く為に作られて、そのC言語自体はデータの流れ(ストリーム)を3つ持っている。それらは「標準入力」「標準出力」そして「標準エラー」と呼ばれていて、原理的には「例外処理」と言うのはデータの流れを「標準エラー」とする、と言うもの。現代のプログラミング言語はC言語で書かれている例が多く、この「例外処理」は原則的にC言語のストリームの扱いに倣っている。
とは言っても、プログラムは「考えられるだけの」誤動作を避けるべき、と言う考え方があり、特に「間違った計算結果」がシステムに重大な障害を及ぼす可能性もある、と言うのは事実。その為、マズい計算結果は標準の入出力を避け、全体に影響を与えない別のストリーム(標準エラー)に流し込んでシステム上の障害を避ける、と言うのがC言語の設計の発想であった。つまり、おかしな挙動をしたプログラムのせいでシステム全体がクラッシュしないように考慮されているのである。そのお陰でC言語で書かれたOSは安全性が高まる、と言うアイディアだった。
では、同じC系の言語で書かれているWindowsが良くフリーズする(固まる)のは何でだろう(笑)?
その辺はMicrosoftの企業秘密なのかもしれない(笑)。


では実際「問題がある」数値を入力してみて、例外処理が発動するかどうか見てみましょう。



はい、キレイに例外処理が発生していますね。当然普通の「目的とする」計算も完璧にこなしてくれています。
お疲れ様です。「十進数をn進数(nは37未満)に変換する」プログラムの完成です。

と、このように、プログラミングの実際は「思いつきを書き直し書き直し」作っていく、と言うのがホントのトコロです。また、「間違ったプログラムを書いちゃう」ってのも当然なんですね。誰でもみんなやることなのです。
従って、「誤った計算結果が出ても」凹まない(笑)。少々プログラミングを間違えたくらいでコンピュータは爆発したりしないんで、プログラムをする側は少々ふてぶてしいくらいの方が良い、と言う実例でした。

以上です。


魔法言語 リリカル☆Lisp


DrScheme

  1. DrScheme での Language Pack の選択

  2. Scheme の単純な式「(+ 3 2)」, 「(- 10 4)」

  3. Scheme の単純な式(括弧の入れ子)

  4. Scheme の単純な式 (DrScheme の定義ウインドウ)

  5. 関数 area-of-disk の定義と,関数適用

  6. 関数 area-of-disk,変数 PI の定義と,関数適用(1)

  7. 関数 area-of-disk,変数 PI の定義と,関数適用(2)


0 コメント: