Powered by SmartDoc

コンピュータ概論(2002/11/13)
Ver. 1.2

2002年11月13日
栗野 俊一
kurino@math.cst.nihon-u.ac.jp
http://edu-gw2.math.cst.nihon-u.ac.jp/~kurino/comp/index.html
コンピュータ概論2002/11/13 の資料

目次

お知らせ

  1. 「聴いて解るか?」という疑問に対しては、「恐らく、解らないでしょう」が回答になりますが、目的は、「解る」ではなく「雰囲気を味あう」ことにあるのですから、「解らなくても気にする必要はない」ということです。
  2. くどいようですが、講義時間にwindows updateを行うのは止めてください。Networkが混む原因になりますから。

    講義終了後(演習の時間)などに、空いている教室などを利用して実行してください。

    僕は、必ず行っています。環境の統一という観点からも、受講者全員がwindows updateを行っていることが望ましいと思います。

演習

数当てゲームを考えてみよう (Lisp 3)

先週にも述べたようにここの演習目的は、必ずしも、Lisp言語を学ぶことにはないのですが、新しい言語を学ぶにしても、Programmingの考え方を学ぶにしろ、共通な点は、いずれにせよ習うより慣れろが基本なので、具体的な例題を考えてみたいと思います。

そこで、昔から良くあるのですが、「数当てゲーム」をLispで作ることを考えてみましょう。

「数当てゲーム」というのは、次のような簡単な(子供が行うような.. )ゲームです。

  1. GameのPlayerは二人で、一人が出題者、もう一人が回答者になります。
  2. 最初に出題者が1から100までの整数値を考え、これを問題とし、回答者からは隠しておきます。
  3. 以下、回答者が、出題者の隠してある数値を当てるまで以下のPlayを繰り返します。
    1. 回答者は、出題者の隠していると思われる数値案を述べます。
    2. 出題者は、その案と正答(隠している数)との関係から次のいずれかを行います。
      一致した場合
      「当り」と言って、終了
      案が正答より大きい場合
      「大きすぎる」と言う
      そうでない場合(案が正答より小さい場合)
      「小さすぎる」と言う

ここでは、このGameの出題者をLisp Programとし、回答者は人間と考えることにしましょう。

数当てゲームの設計

上記のProgramを考える上で、まず、基本的な仕組みを色々しらなければなりません。

たとえば、次のような項目がそれぞれ必要となるでしょう。

出力
どうやって、回答者(人間)にメッセージ(「当り」、「大きすぎる」、「小さすぎる」等)を送るか?
入力
どうやって、回答者(人間)から答案を得るか?
問題作成
どうやって、問題を作るか?
順接
二つ(以上)のことを、続けて行うはどうすればよいか?
判断とその反映
どうやって、回答と正答を比較するか?、また、判断した結果、どのようにして、振る舞いを変えるか?
繰り返し
同じことを繰り返すにはどうすればよいか?

そこで、順番に説明して行くことにします。

Lisp の入出力

出力

Lispで出力を行う場合は、組み込み関数(3)を利用します。xlispでは、次の関数が出力用の関数が利用できます。

いずれにせよ、関数の値は、引数があれば、その値をそのまま返し、出力は、副作用(4)として、実現されます。

princ
引数は1つで、その値を出力します。
出力関数 princ
> 1
1
> (princ 1)
1
1
> (princ "abc")
abc
"abc"
> (setq x 12)
12
> x
12
> (princ x)
12
12
>
terpri
画面に改行を出力します。ようするに、見かけ上、行のおり返しを行います。値は、NILになることに注意。
出力関数 terpri
> NIL
NIL
> (terpri)

NIL
> (princ NIL)
NIL
NIL
>
  1. 組み込み関数というのは、Lispシステムに予め組み込まれているため、特に何もしなくてもいきなり利用できるような関数と理解してください。
  2. 副作用というのは、「薬の?」のような使い方をし、あんまり良い意味では使いませんが、「本来の目的ではないが、生じてしまうこと」という意味です。

    Lispでは、関数に引数を与えてその評価結果(関数値)を得ることが、本来の関数の意味なので、その意味で、出力関数は、何ら意味を持ちません(まあ、要するに恒等関数なのですから..)し、「出力を行う」という機能は、(関数値を求めることではないという意味で..)副作用と解釈されるわけです。

入力

Lispの入力もまた、組み込み関数を利用します。入力というのは、人間がキーボードからいくつかの文字を入れた後に、Enter Keyを押すと、その文字列が何らかの形で、Lispに取りこまれるということです。

今回の場合は、数値ですが、以下のread関数を利用すると、キーボードから、数値アトムを意味するような文字列(5)を入れ、Enter Keyを押すだけで、read関数の値は、その数値アトムに評価した値となります。

read
引数はなく、キーボードから、入力された結果を値とする。
入力関数 read
> (read)
1
1
>
  1. 後から解るように、一般にS-式を意味する式を入力することが可能で、readの結果もS-式になります。

乱数の発生

これは、基本的な機能ではないのですが、数当てゲームでは必要なので、おまけとして説明します。

問題を作成するには、計算機が、適当(6)に数を生成する必要があります。

今回は、組み込み関数randomを利用します。

random
引数で指定した値未満未満の正の整数値を持つ乱数値を値とします。
乱数生成関数 random
> (random 100)
92
> (random 100)
24
> (random 100)
61
> (random 100)
8
> (random 1)
0
> (random 1)
0
>
  1. 「適当」というのは、人間には、それほど難しい話ではないのですが、計算機には大変難しいことのひとつです。幸い、今回の場合は、「意味がなくてもよい」ので、乱数が利用できます。

    実を言えば、「乱数」自身も、大変難しい仕組みなのですが、一応、Lispには、お仕着せのものがあるので、(その良し悪しはともかく..)それをそのまま、利用してしまいましょう。

    このように、「本来は、それを使って良いのか?」という判断は大変重要かつ、コストの掛かる作業なのですが、「一旦、そこで思考を止めて、まあ、今回はこれにしよう。問題があるなら後でなんとかしよう」という、「棚上げ思考」は、Programmingを行う上で、有効かつ良く利用される思考法であることも、覚えておきましょう(ただ、「棚上げしていたことを忘れてしまう」という問題が、しょっちゅう生じてしまうという困った性質もあるのですが.. )。

複数のことを続け行う

何かをした後に、別のことをする。これは、Programmingの中でも、最も基本的な操作の一つです。

Lispでは、これを関数合成で実現するのが普通です。たとえば、「3に1を足して2倍する」というのは、「1を加える」という作業と「2倍する」という作業を続けて行うわけですが、これをLispでは、関数*と+の合成、すなわち、(* (+ 3 1) 2)で実現します。

3 に 1 を足して 2 倍する
> (+ 3 1)
4
> (* 4 2)
8
> (* (+ 3 1) 2)
8
>

しかし、出力のような副作用を持つ関数の場合は、どうでしょうか? abcだして、次にefgを出す(つまり、結果的にabcefgを出力する)には.. ?

lispでは、値ならば関数を合成することによって、実現できますが、副作用は、値ではないので、このような単純な関数合成では実現できません。

組み込み関数or, andには、次のような約束があるので、その約束と副作用を組み合わせることにより、この問題に答えることができます。

orやandの振る舞い
orもandも複数の引数をもち、その値を論理値として、論理和、論理積を計算するが、引数を評価する順番は、左から右の順に行うことが決まっており、もし、その引数が副作用を起こすのであれば、その副作用が生じる順番も左から右の順になる。

したがって、たとえば、次のような形で、andを利用することができます。

and を利用した順接
> (princ "abc")
abc
"abc"
> (princ "efg")
efg
"efg"
> (and (princ "abc") (princ "efg"))
abcefg
"efg"
> (and (princ "abc") (not (terpri)) (princ "efg") (princ "xyz"))
abc
efgxyz
"xyz"
>

応答プログラム

入出力と順接を学べば、次のような典型的な応答プログラムを作成することができます。

応答プログラム
メッセージを出力後、入力を促し、その入力結果を出力して終了するようなプログラム。
応答プログラム例(入力した数の2倍を答える)
プログラムの流れ
  1. プログラムに説明を出力
  2. 入力を促す
  3. 入力から計算( 2倍)した結果を出力する
プログラム例
(defun ans-two-time()
	(and
		(and (princ "times 2" ) (not (terpri)))
		(princ "Please Input Number: " )
		(ans-two-time-print (read)) ))

(defun ans-two-time-print ( n )
		(and
			(princ "Answer is " )
			(princ (* n 2))
			(princ "." )
			(not (terpri)) ))

実行例
応答プログラム
> (ans-two-time)
times 2
Please Input Number: 2
Answer is 4.
T
> (ans-two-time)
times 2
Please Input Number: 15
Answer is 30.
T
> 

判断とその反映

「判断」をするには、いわゆる、述語関数が利用できます。これは、別の言い方をすれば、その関数の結果がTかNILになるような関数一般をさします。

たとえば、等しいかどうかを判定したり、大小関係を比較するなどが典型的です。

整数値の比較に関しては、すでに、= (等しい) > (大きい) < (小さい)などがあるので、これを利用するだけです。

問題は、「その判断結果をどう利用するか?」ですが、そのために利用される組み込み関数がifです。このif関数を利用すれば、条件の成立不成立の違いによって、異なる振る舞いをプログラムにさせることができます。

if

引数は、2つか3つだが、2つの場合は、単に3つ目にNILが指定された場合と同じ振る舞いをする。

ifは最初に、1つ目の引数を調べ、それがNILかどうかを判断する。NILでなければ、2つ目の引数を評価し、その値が全体の値となる。そうでなければ、3つ目を評価し、その値が、全体の値となる。

注意すべきことは、「副作用は、評価された時点で有効になる」ということで、たとえば、一つ目の引数の値がNILでない場合、3つ目の引数が副作用があるようなものでも、それが評価されないので、結果的に副作用自身も生じない。

ifを利用した応答プログラム例
プログラムの説明
入力した数によって、正負を答える。
プログラムの流れ
  1. プログラムに説明を出力
  2. 入力を促す
  3. 入力によって、次のいずれかを行う。
    0以上の時
    正だという
    そうでないとき
    負だという
プログラム例
(defun ans-sign()
	(and
		(and (princ "sign" ) (not (terpri)))
		(princ "Please Input Number: " )
		(ans-sign-print (read)) ))

(defun ans-sign-print (n)
	(and
		(princ "Answer is " )
		(if (<= 0 n)
			(princ "positive")
			(princ "negative") )
			(princ "." )
			(not (terpri)) ))

実行例
if による応答プログラム
> (ans-sign)
sign
Please Input Number: 2
Answer is positive.
T
> (ans-sign)
sign
Please Input Number: -3
Answer is negative.
T
> 

繰り返し

Lispにおいて、同じことを繰り返すのは、「再帰呼び出し」を利用します。

これも、説明するより、実際にやった方が簡単なので、例で示すことにします。

「入力をそのまま出力する」という作業を繰り返す関数
プログラムの流れ
  1. 入力した結果を出力する
  2. もう一度(再帰呼び出し)
プログラム例
(defun echo-loop ()
        (and
		(echo-print (read))
		(echo-loop) ))

(defun echo-print ( n )
	(and (princ n) (not (terpri))) )

実行例
このプログラムは、無限にとまらないので、Ctrl-C ([Ctrl] keyを押しながら[c] keyをポンと押す)で強制終了します。
無限の繰り返し
> (echo-loop)
1
1
2
2
123
123

[ back to top level ]
> 

つまり、「定義する関数の中で、自分自身を利用することによって、繰り返しが実現できる」わけです。

もちろん、このように、無限に繰り返すような形では、色々と不便なので、通常は、if (繰り返しを続けるか、それとも止めるかの判定をさせる)と組み合わせることによって、「必要回数の繰り返し」が可能になります。

「0以外の入力をそのまま出力する」という作業を繰り返す関数
プログラムの流れ
  1. 入力によって、次のいずれかを行う。
    0の時
    なにもせずに終了
    0以外
    1. 入力した結果を出力する
    2. もう一度(再帰呼び出し)
プログラム例
ここで、注意したいことは、readで読み込んだ数を二度( ifの条件と、出力のところの2個所)で利用しているので、別の関数( echo-non-zero-check )を作成し、そこに入力を引数として与えている。引数として渡せば、渡された側で、同じ値を何度も参照できるので、「同じ値を二度(以上)利用する」ことが可能になる。
(defun echo-non-zero-loop () ( echo-non-zero-check (read) ))

(defun echo-non-zero-check ( n )
	(and
		(if (= n 0)
			nil
			(and (princ n) (not (terpri))) )
        	(echo-non-zero-loop) ))
実行例
条件付きの繰り返し
> (echo-non-zero-loop)
1
1
2
2
0
NIL
> 

数当てプログラムの作成

基本的な内容は、上記で説明したので、いよいよ、数当てゲームです。この関数も、いくつかの関数の組み合わせで作成することを考えます。

Game の繰り返し部分

まず、Gameの繰り返し部分から着目しましょう。問題は引数で与えられているものとし、入力によって、振る舞いを変えるというものです。

基本は、一つ前の「条件付き繰り返しと同じ」なので、次のような感じになります。

(defun number-game-loop ( q )
	(number-game-check q (read)) )

(defun number-game-check (q a)
	( if (= q a)
		(and (princ "Hit") (not (terpri)))
		(and
			(if (< q a)
				(princ "Big" )
				(princ "Small" ) )
			(not (terpri))
			(number-game-loop q) )))

後は、問題を生成して、上記の関数を呼ぶだけですから、次のような感じになります。

(defun number-game-main ()
	(number-game-loop (+ 1 (random 100))) )

作成例

更に、「何回目の試行か?」、「もう一度ゲームをするか?」などの機能を追加した版をここ置きましたので参考にしてください。

xlisp の演習

lispを参照して、先週に引き続き、2章の演習問題( [演習問題2.x.y]とある問題)を全て解いてみてください。

課題提出

「lispの演習問題の結果」をreportとして、送ってください。

内容

提出内容は「lispの演習問題の結果」です。具体的には、xlispに対して、行った入力と、その結果をcopyしたものでOkeyです。例えば、演習1.1.1であれば、次のような形でOkeyです。

> (+ 123 456)
579
>

ただし、適当に空行を入れて、課題と課題の区別がつくようにしてください。

形式

以前、紹介したsample.xmlを参考(7)に、XML文章を書きます。

<author>の所
当然ながら、自分の名前、学生番号学科を書く
<id>の所
20021113001
<title>の所
lisp演習(1)
<date>の所
2002/11/13
<body>の<contents>の場所
上記のxlispとのやりとりのcopy

なお、reportとして、提出するe-mailのSubjectには、必ず、[R]から始まる、わかり易いSubjectを付けてください。

提出方法
提出は、e-mailで行いますが、次の点に注意してください。
宛先
math-comp-examine@media.cst.nihon-u.ac.jp
表題( Subject )
[R]から始まる、わかり易いSubject (例「[R] lisp課題1」など)
内容
課題のXML文章そのもの(送付書類は不可)
電子署名
clear署名を付けること。なお、その公開鍵は、Webを登録するのに用いるものと同じものとする。
  1. このテキストを利用する時には、[ファイル]→[名前を付けて保存]とするか、[表示]→[ソース]としてから、マウスでカットアンドペーストしてください。なにもせずに、そのままマウスでカットアンドペーストすると変なものになります。