目次へ 最終更新 : 2003/7/28
第5章 対話型プログラム
訳者注)セクション5.4.2と5.5.3のプログラムには間違いがありましたので、コンパイル可能なように訳者が一部修正しています。
前章では、どのようにして一意性型付けが、安全な方法で、ファイルやウィンドウのようなオブジェクトの操作に使用できるかを示した。本章では、より詳細に、これがファイルI/Oを行うのにどのように使用できるかを説明する。更に、CLEANのような純粋関数型言語で、どのように対話型プログラムを記述できるかを説明する。
ウィンドウアプリケーションを記述することは単純な仕事ではない。生活をより楽にする為に、対話型アプリケーションを高水準抽象でプラットフォーム非依存に記述できる関数及びデータ構造を提供する大きなライブラリ(オブジェクトI/Oライブラリ)が、CLEANで記述されている。本章は、いくつかの例を持ち、CLEANライブラリの提供する機能性を例解する。
CLEANライブラリの重要な利点は、同一のプログラムが、異なったマシン(例えば、Macintosh、Windows '98)上でも何の変更もなく動作できるということである。各マシン上では、結果のメニュー、ダイアログ及びウィンドウは、そのマシンに一般的なルック・アンド・フィールに従っている。
ウィンドウベースの対話型アプリケーションを書くことは、確実にどうでもよい仕事ではない。CLEANオブジェクトI/Oライブラリを使用して実世界のアプリケーションを書きたいと思うプログラマに対しては、Peter AchtenのオブジェクトI/Oリファレンスマニュアルを読むことを勧める(最新情報については、www.cs.kun.nl/~clean参照)。
5.1 世界のファイルを変更する
オブジェクトI/Oライブラリに行く前に、標準環境が提供するような普通のファイル入出力をより詳細に見ることから始める。完全なファイルをコピーするプログラムを書きたいものとする。前章の例の拡張版を定義することは容易なので、1つ又は2つの文字が書込まれるだけではなく、文字の完全なリストがファイルにコピーされる。これを、ファイルから文字を読込む関数と組み合わせると、ファイルをコピーする関数が得られる。
CharListWrite :: [Char] *File -> *File
CharListWrite [] f = f
CharListWrite [c:cs] f = CharListWrite cs (fwritec c f)
CharFileCopy :: File *File -> *File
CharFileCopy infile outfile = CharListWrite (CharListRead infile) outfile
ファイルから文字を読込むことは、書込みよりも数行多く必要とする。というのも、読込みに対しては、環境(そのファイル)が渡されなければならないだけでなく、結果が出力されなければならないからである。文字の読込元のファイルは、一意である必要はない。というのも、破壊的更新は読込みには関わっていないからである。
CharListRead :: File -> [Char]
CharListRead f
| not readok = []
| otherwise = [char : CharListRead filewithchangedreadpointer]
where
(readok,char,filewithchangedreadpointer) = sfreadc f
読込み関数は遅延である。従って、文字が評価に必要である場合に一文字ずつファイルから読込まれる(sfreadcの実際のライブラリ実装はおそらくある種のバッファリングスキームを使用しているだろうが)。
これでファイルコピー関数は完成したが、まだファイルコピープログラムを持ってはいない。無いものは、問題のファイルを開閉する関数であり、勿論、ファイルシステムにアクセス可能であるようにアレンジしなければならない。ファイルを開閉するコピー関数は、以下に示す。これは、引数として一意なファイルシステムを取り、結果としてそれを出力する。クラスFileSystemのインスタンスである各環境は、ファイルシステムをモデル化している。クラスFileSystemは、StdEnvの一部であるStdFileの中に定義されている。このモジュールは、FilesとWorldのインスタンスを提供している。これは、ファイルシステムにアクセスする為に、世界からそのファイルシステムを抽出しなくともよいということを示している。
CopyFile :: String String *Files -> *Files
CopyFile inputfname outputfname filesys
| readok && writeok && closeok
= finalfilesystem
| not readok = abort ("Cannot open input file: '" +++ inputfname +++ "'")
| not writeok = abort ("Cannot open output file: '" +++ outputfname +++ "'")
| not closeok = abort ("Cannot close output file: '" +++ outputfname +++ "'")
where
(readok,inputfile,touchedfilesys) = sfopen inputfname FReadText filesys
(writeok,outputfile,nwfilesys) = fopen outputfname FWriteText touchedfilesys
copiedfile = CharFileCopy inputfile outputfile
(closeok,finalfilesystem) = fclose copiedfile nwfilesys
入れ子通用範囲を使用すると、関数CopyFileは、もう少しエレガントに書くことができる。ファイルシステムの様々なバージョンに対して名前を発明する必要は無い。このバージョンは、前の関数と構文的にのみ異なっているに過ぎないということに注意。ファイルシステムの様々なバージョンは依然として存在するが、全てのバージョンは同じ名前を持っている。'#'定義の通用範囲は、どのバージョンが使用されているかを決める。
CopyFile :: String String *Files -> *Files
CopyFile inputfname outputfname files
# (readok,infile,files) = sfopen inputfname FReadText files
| not readok = abort ("Cannot open input file: '" +++ inputfname +++ "'")
# (writeok,outfile,files) = fopen outputfname FWriteText files
| not writeok = abort ("Cannot open output file: '" +++ outputfname +++ "'")
# copiedfile = CharFileCopy infile outfile
(closeok,files) = fclose copiedfile files
| not closeok = abort ("Cannot close output file: '" +++ outputfname +++ "'")
| otherwise = files
この定義では、ライブラリ関数fopenとsfopenが、ファイルを開くのに使用される。それらの違いは、fopenは、ファイルが一意であることを必要とするが、sfopenは、ファイルの共有を認めているということである。両方の関数ともファイルが使用される方法を指示する引数属性を持っている(FReadText, FWriteText)。もう1つの可能な属性はFAppendTextであろう。似たような属性は、データを持つファイルを扱う為に存在する。
ファイルシステムそれ自体にアクセスすることは、プログラムの外部の世界にアクセスすることを意味している。これは、Start規則がマシンの完全なステータスをカプセル化する抽象引数Worldを持つのを認めることで、可能となる。また、この世界は、抽象データ型によって表現される。
図5.1 ファイルシステムをカプセル化する抽象型World

一意な環境渡しを使用するので、その環境内に保持されるファイルシステムを変更する新しい世界を意味的に作り出す関数(appFiles)が、ライブラリの中に定義された。CLEANプログラムの最終結果は、変更したファイルシステムを持つ世界である。
inputfilename :== "source.txt"
outputfilename :== "copy.txt"
Start :: *World -> *World
Start world = appFiles (CopyFile inputfilename outputfilename) world
これは、ファイルコピープログラムを完成させる。コンテクストによってはより適切かもしれない、ファイルを読込む他の方法は、行毎又はメガバイト毎である。これは確実に、一文字毎にファイルを読込むよりも効率的である。これに対応する読込み関数を以下に示す。
LineListRead :: File -> [String]
LineListRead f
| sfend f = []
# (line,filerest) = sfreadline f // 行はまだ改行文字を持つ
= [line : LineListRead filerest]
MegStringRead :: File -> [String]
MegStringRead f
| sfend f = []
# (string,filerest) = sfreads f MegaByte
= [string : MegStringsRead filerest]
where
MegaByte = 1024 * 1024
上に示した関数は遅延である。従って、ファイルの関連部分は、プログラムの評価に必要な場合にのみ読込まれる。
何かをする前に、ファイルを完全に読込むことが求められる場合も時々あるかもしれない。以下では、一度にファイル全体を読込む正格読込み関数を示す。
CharListReadEntireFile :: File -> [Char]
CharListReadEntireFile f
# (readok,char,filewithchangedreadpointer) = sfreadc f
| not readok = []
#! chars = CharListReadEntireFile filewithchangedreadpointer
= [char : chars]
#!による構成物(正格let構成物)は、定義された値が後で使用されているか否かに関わらず、その評価を強制する。
Hello World
計算機科学の古典的で有名な演習は、ユーザーにhello worldのメッセージを示すプログラムを作ることである。これを行う最もシンプルなCLEANプログラムは、勿論以下のものである。
Start = "hello world"
この結果はコンソール上に表示される(このオプションがプログラム環境に設定されていることを確かめて欲しい)。ユーザーにこのメッセージを示すもっと複雑な方法は、明示的にコンソールを開くことである。コンソールは、ちょうどファイルのように扱われている。StdFileに定義された読み書き関数を使用することにより、コンソールから情報を読込み、コンソールに情報を書き込むことができる。コンソールに次々に読み書き関数を適用することで、読み書きの間の簡単な同期化を達成することができる。これを以下のプログラムに示す。"file"コンソールを開くには、関数stdioをその世界に適用しなければならない。コンソールは、関数fcloseを使用することで閉じることができる。
module hello1
import StdEnv
Start :: *World -> *World
Start world
# (console,world) = stdio world
console = fwrites "Hello World" console
(ok,world) = fclose console world
| not ok = abort "Cannot close console"
= world
この定義では、再びlet式の入れ子通用範囲を使用した。そこで、コンソールと世界の名前を再利用することができる。更に、動作が記述される順序は、コンソールの入出力が発生する順序に対応している。
ユーザーの名前を読込み、個人的なメッセージを生成することで、この例を少し拡張できる。ここで、入出力両方を行う単一のファイルとして、コンソールが何故使用されるのかが明確になる。ユーザーの名前を読込むことは、メッセージ"What is your name?"をコンソールに書き出した後にのみ行うことができる。一意なコンソール渡しによって分かるデータ依存関係は、自動的に、求める同期化を確立する。
module hello2
import StdEnv
Start :: *World -> *World
Start world
# (console,world) = stdio world
console = fwrites "What is your name?\n" console
(name,console) = freadline console
console = fwrites ("Hello " +++ name) console
(_,console) = freadline console
(ok,world) = fclose console world
| not ok = abort "Cannot close console"
| otherwise = world
このプログラムでは、メッセージをユーザーに書き出した後にプログラムを待たせる為に、第2のreadline動作を加えた。
5.2 環境渡し技術
以下の定義を考える。
WriteAB :: *File -> *File
WriteAB file = fileAB
where
fileA = fwritec 'a' file
fileAB = fwritec 'b' fileA
WriteAB :: *File -> *File
WriteAB file = fwritec 'b' (fwritec 'a' file)
WriteAB :: *File -> *File
WriteAB file
# file = fwritec 'a' file
file = fwritec 'b' file
= file
これらは、環境渡し関数を持つ少し異なったプログラミングスタイルを使用しており、同等である。
最初のものの欠点は、新しい名前fileAやfileABを発明しなければならないということである。もしこのようなスタイルが大規模プログラムで使用されると、file1やfile2(又はfile'やfile''のようなものさえ)のようなより不明確な名前を提案しがちになる。これは、何が起きているのかの理解を困難にする。
第2のスタイルは、これを回避しているが、欠点として、関数合成の読込み順序が、関数適用が実行される順序の逆であるということがある。
第3のスタイルは、入れ子通用範囲スタイルを使用する。これは非常に読みやすいが危険でもある。というのも、同一名を複数の定義で再利用できるからである。エラーが容易に作られる。もし名前の再利用がファイル、世界やコンソールのような一意なオブジェクトに制限されるなら、型システムは多種の型エラーを検知できる。もし他の名前も(命令型プログラミングスタイルに非常に似ている)このプログラミングスタイルで再利用されるならば、型システムが容易に検知できない型エラーを引き入れるかもしれない。
以下では、これと同一の関数を定義する他のいくつかのスタイルを示す(というのも、ファイルに文字を書き込むことに関して、この最後のスタイルの1つは、上記欠点を避けるので、より望ましいからである)。
WriteAB :: (*File -> *File) // ()は、関数がアリティ0で定義されることを示す
WriteAb = fwritec 'b' o fwritec 'a'
seqは、状態遷移関数のリストを連続して適用する。関数seqは、以下のように定義される標準ライブラリ関数である。
seq :: [s -> s] s -> s
seq [] x = x
seq [f:fs] x = seq fs (f x)
関数seqを使用したWriteABのいくつかの代替的定義。
WriteAB :: (*File -> *File)
WriteAB = seq [fwritec 'a', fwritec 'b']
WriteAB :: *File -> *File
WriteAB file = seq [fwritec 'a', fwritec 'b'] file
WriteAB :: (*File -> *File)
WriteAB = seq o map fwritec ['ab']
ファイルに情報を書き込む便利な方法は、多重定義中置演算子<<<を使用することである(StdFile参照)。これは、左手側のファイルと、右手側のファイルに書き込む(<<<のどの型のインスタンスが存在しているかの)値を前提としている。これは以下のように使用できる。
WriteAB :: *File -> *File
WriteAB file = file <<< 'a' <<< 'b'
WriteFac :: Int *File -> *File
WriteFac n file = file <<< " The Value of fac " <<< n <<< " is " <<< fac n
多重定義<<<演算子を使用することの利点は、プログラマがユーザー定義の代数データ構造に対するインスタンスを定義できるということである。
5.2.1 入れ子通用範囲スタイル
ファイルに書込むために、ある値を文字列に変換する、上記関数に似た型の関数は、seq又は<<<のような関数を使用することで、容易に組み合わせることができる。しかし、ファイルから情報を読込む関数は、ほとんどおそらく異なった型の結果を作り出す(例えば、freadcは文字を読込むが、freadは文字列を読込む)。そのような関数を組み合わせることは一層複雑である。これを行う最も直接的な方法は、入れ子通用範囲スタイルを使用することである。
2つの文字が後続する整数値からなるファイルから、zipコードを読込みたいとする。以下の例は、これをどのようにしてCLEANで書くことができるのかを示す。
readzipcode :: *File -> ((Int, Char, Char), *File)
readzipcode file
# (b1,i, file) = freadi file
(b2,c1,file) = freadc file
(b3,c2,file) = freadc file
| b1 && b2 && b3 = ((i,c1,c2), file)
| otherwise = abort "readzipcode failure"
('#'により指示される)let構成物では、読込み動作の結果を容易に指定できる。読込み動作が実行される順序は、一意に属性付けられたfile引数を、シングルスレッド的に渡すことで制御される。
5.2.2 モナドスタイル
CLEANの一意性型付けのおかげで、ファイルのような破壊的オブジェクトを明示的に渡すことができる。しかし、大部分の他の関数型言語には、強力な型システムが欠けている。従って、どのようにして、参照透明性を失うことなくファイルのようなオブジェクトの破壊的更新を提供するのだろうか?ここで、ちょうど良いトリックがある。つまり、オブジェクトを明示的には渡さないが、プログラマから隠蔽して黙示的にそれを渡すというものである。もしオブジェクトがプログラムを通して隠蔽されているままならば、それは共有されることはない(ユーザー定義の関数が明示的にそれを引数として持っていないので)。この方法では、参照透明性が保証される。このアプローチは、遅延の純粋関数型言語HASKELLで採用されている。それはモナドアプローチと呼ばれる。全ての更新可能なオブジェクトは、CLEANのWorldと同等の1つの大きな状態の中に隠されている。しかし、モナドアプローチでは、この状態は隠蔽され、それを使用する関数によって明示的にアクセスされることはない。その関数の集合は、モナドと呼ばれる。そのような関数はモナド関数とも呼ばれる。HASKELLプログラムは、この隠蔽されたモナドに適用されるモナド関数を出力する。そのような関数は型を持つ(状態が隠蔽されているIOモナドの型も示される)。
:: Monad_function s a :== s -> (a,s) // IO a :== Monad_function World a
解決すべき主要な問題は以下のものである。つまり、もしこの隠されているモナドにアクセスできないならば、モナドの中に隠されているファイルから値を読込むことはどのようにしてできるのだろうか?bindと呼ばれる、特別な方法で2つのモナド関数を組み合わせることのできる特別な演算子が、このトリックを果たす。
:: St s a :== s -> (a,s)
(`bind`) infix 0 :: (St s a) (a -> (St s b)) -> (St s b)
(`bind`) f_sta a_fstb = stb
where
stb st = a_fstb a nst
where
(a,nst) = f_sta st
return :: a -> (St s a)
return x = \s -> (x,s)
bind関数は、2つのモナド関数が次々にモナド上で適用されるということを保証する。そして、非常に利口なことに、その最初のモナド関数の結果は、追加的な引数として、第2モナド関数に与えられる!この方法で、第2モナド関数は、その前のモナド関数の結果を検知できるのである。
CLEANでは、(状態モナドを実際に隠すことを強制されることなく)モナドスタイルプログラミングも使用できる。zipコードは以下の方法でモナドスタイルでファイルから読込むことができる。
readzipcode :: (*File -> ((Int,Char,Char),*File)) // St *File (Int,Char,Char)
readzipcode
= freadint `bind` \(b1,i) ->
freadchar `bind` \(b2,c1 ->
freadchar `bind` \(b3,c2) ->
if (b1 && b2 && b3) (return (i,c1,c2))
(abort "readzipcode failure")
where
freadint file = ((b,i),file1) where (b,i,file1) = freadi file
freadchar file = ((b,c),file1) where (b,c,file1) = freadc file
この例は、様々な型の結果を作り出す状態遷移関数が、CLEANでは、どのようにしてモナドプログラミングスタイルに組み合わせることができるのかを示している。
モナドアプローチの利点は、一意性型付けのような追加的な型システムが、純粋関数型言語での安全な入出力を保証する必要がないということである。その欠点は、プログラマによる一意なオブジェクトの複製を不可能にする為には、これらオブジェクトがプログラマから隠蔽されたままでなければならないということである。従って、更新される全てのオブジェクトは、実行時システムによってのみアクセスだけができる、1つの隠蔽されたデータ構造たるモナドに保存される。モナドには、プログラマが直接にアクセスすることはできない。間接的にアクセスし変更することができるだけである。これを行う為には、プログラマは、隠蔽されたモナドに関して、そのシステムによって適用されるモナド関数の組合せを提供しなければならない。従って、モナドスタイルは、(自由に渡すことができ、直接アクセスできる任意数の更新可能オブジェクトを可能にする)CLEANの一意性型付けシステムの提供する解決法よりも柔軟ではない。もし望むならば、CLEANでもモナドスタイルでプログラムすることは可能である。モナドは、ユーザーから隠されているかもしれないが、隠される必要はない。一意性型付けは、モナドが正しく扱われることを保証するのに使用できる。
5.2.3 プログラム実行のトレース
正直でいよう。プログラムがどんどん大きくなっている時、使用されている全関数の正当性を形式的に証明することはできない。将来には、自動の証明システムが、正当性証明に際してプログラマを補佐するに十分なほどパワフルになっていると望む。しかし、注意深いプログラム設計、型正当性及び注意深いテストにもかかわらず、実際には、プログラムの非常に大きな部分が依然としてエラーを持っている。エラーが発生すると、どの関数がエラーを引き起こしているのか発見しなければならない。大規模プログラムでは、そのような関数を見つけることは困難である。従って、プログラムで起こっていることをトレースすることが簡便であるかもしれない。一般に、そのトレースを確保する為には、プログラムのファイル又はリストを渡さなければならないので、これには、プログラムの実質的な再設計を必要とする。幸運なことに、ファイルの環境渡しには1つの例外がある。特別なファイルstderrを明示的に開く必要なくして、そのファイルに情報を書き込むことが常にできる。そのトレースは、情報をstderrに書込むことで把握できるのである。
例として、シンプルなフィボナッチ関数のトレースを構築する方法を示す。
fib n = (if (n<2) 1 (fib (n-1) + fib (n-2))) ---> ("fib ", n)
Start = fib 4
これは以下のトレースを出力する。
fib 4
fib 2
fib 0
fib 1
fib 3
fib 1
fib 2
fib 0
fib 1
通常は以下のようにこのトレースを書く。
Start
→ fib 4
→* fib 3 + fib 2
→ fib 3 + fib 1 + fib 0
→ fib 3 + fib 1 + 1
→ fib 3 + 1 + 1
→ fib 3 + 2
→* fib 2 + fib 1 + 2
→ fib 2 + 1 + 2
→* fib 1 + fib 0 + 1 + 2
→ fib 1 + 1 + 1 + 2
→ 1 + 1 + 1 + 2
→* 5
このトレースから、演算子'+'は、最初にその第2引数を評価していることは明らかである。トレース関数--->は、StdDebugで定義されている関数trace_nに基づく多重定義中置演算子である。それは、結果としてその左手側を出力し、副作用として、stderrに、トレースとしての右手側を書込む。
(--->) infix :: a b -> a | toString a
(--->) value message = trace_n message value
5.3 イベント処理
前セクションでは、ファイルシステムを変更するプログラムを書く方法を示した。しかし、ファイル名はプログラムに直接コード化された。勿論、ダイアログを使用することで、対話的な方法でファイル名を引数に指定したいであろう。この為には、世界からファイルシステムを先取りするだけでは十分ではない。また、プログラムのユーザーが生成したイベント(つまり、キータイプ、マウスクリック及びタイマイベント)は、プログラムによってアクセス可能でなければならず、そして、画面上に適切な応答が出なければならない。イベントは、イベントキューに集められる。このキューは、時間順にそのイベントを記憶する。プログラムは、与えられた順序でこれらのイベントを処理しなければならない。
CLEANで書かれたオブジェクトIOライブラリは、グラフィカルユーザーインターフェースを備えた柔軟でプラットフォーム非依存なプログラムを書くことを比較的容易にしている。中心概念は、そのライブラリがイベントキューを管理しているということである。このライブラリを使用したユーザープログラムは、ウィンドウ、ダイアログ及びプルダウンメニューのようなオブジェクトを定義する。各オブジェクトは、そのようなオブジェクトに関連する様々なイベントを処理する関数を持つ。IOライブラリは、現在のイベントを選択し、それに適切なイベントハンドラ関数を見つけることに注意を払う。また、画面上のマウス移動、プルダウンメニューのロール、ダイアログ内の選択されたボタンのハイライト、ダイアログの編集コントロール内部での編集及び、ウィンドウの再描画のような、全ての低水準IO演算は、ユーザープログラムの外側で行われる。
多くのデバイスは、それらの状態を記述する情報を持つ。そのような状態は、例えば、ウィンドウ内に描かれる物やこれらの物が保存されるファイル名を定義する。これらの状態は、イベントが発生し、関連するイベントハンドラによって変更できる時には利用可能でなければならない。IOライブラリは、これらの状態も処理する。各イベントハンドラは、引数として、デバイスの現在の状態を受け取り、新しい状態を作り出す。実際には、2つの状態が関わっている。つまり、デバイスの局所状態と、IOプロセスの大域状態である。良いソフトウェア工学の慣行によれば、デバイスに関わるに過ぎない全ての物は、その局所状態内に保存されなければならない。この方法では、プロセスの大域状態が細かく取り散らかっているということを防止する。複数のデバイスにインパクトを持つ物だけが、大域プロセス状態に保存されなければならない。この「大域」プロセス状態は、今度は、そのプロセスに局所的である。
CLEANでは、通常のデータ構造や関数を使用するとデバイスが指定される、ということを理解することは重要である。このことは、デバイス定義がちょうどCLEANの他の全てのデータ構造のように操作できるということを示している。この方法で、全種のデバイスを動的に生成できる!
高水準なダイアログやウィンドウの指定方法によって、プラットフォーム非依存な方法でそれらを定義することが可能になる。つまり、ダイアログやその他のデバイスのルック・アンド・フィールは、使用される具体的なOSに依存する。CLEANのコードは、CLEANシステムが利用可能なあらゆるプラットフォームで変更なく使用できる。結果のアプリケーションは、そのプラットフォームに典型的な適切なルック・アンド・フィールを持っているだろう。
抽象デバイス仕様で使用される各イベント処理関数は、(PStと呼ばれ、プロセス状態と言う)プログラムの現在の状態を表している一意な引数を持つ。このプロセス状態それ自体は、局所論理状態と、対話型プログラムの(抽象デバイスとも呼ばれる)GUIコンポーネントの状態の(IOStと呼ばれ、入出力状態と言う)抽象からなる。
図5.2 プロセス状態PStとそのコンポーネント

イベントベースのインターフェースを持つ各プログラムの対話プロセスは、ライブラリStdIOからインポートされる関数startIOによって初期化される。この関数の型は以下のものである。
startIO :: !DocumentInterface !.l !(ProcessInit (PSt .l)) ![ProcessAttribute (PSt .l)]
!*World -> *World
この最初の引数は、プログラムの提供するインターフェースの種類を決める。Windowsの提供するインターフェースのタイプに影響を受けた3つの可能性がある。
:: DocumentInterface
= NDI // No Document Interface
| SDI // Single Document Interface
| MDI // Multiple Document Interface
ユーザーに非常に限られた対話しか持たないプログラムは、No Document Interface、つまり、NDIを使用する。テキスト編集又は描画の為の汎用ウィンドウはどれも利用できないし、それに関連するメニューボークも利用できない。ユーザーとの全ての対話は、ダイアログによって行われる。Single Document Interface、つまり、SDIを備えるプログラムは、テキスト編集又は描画にシングルウィンドウを持つことができる。また、メニューボークも持つ。Multiple Document Interface、つまり、MDIは、アプリケーションウィンドウ内に複数のウィンドウを持つことができる。ウィンドウは、これらウィンドウを操作するWindowメニューを自動的に提供する。
ライブラリ関数startIOの第2引数は、プログラムの初期論理状態である。この状態は、プログラムにおいて大域である全ての情報を持ち、イベントの間で記憶されていなければならない。通常この情報は、レコードに保存される。この引数の型は、型変数lであり、これは、あらゆる型を使用できるということを示している。パフォーマンスの理由から、この引数は正格である。
startIOの第3引数は、型ProcessInit (PSt .l)を持ち、これは、(PSt .l) -> (PSt .l)と同等である。これらの関数は、ダイアログやメニューのような初期プロセス対話コンポーネントを生成する。すぐにいくつかの具体例を見る。
第4引数は、プロセス属性のリストである。これらの属性は、ウィンドウの位置及びサイズのような問題を制御する。非常に頻繁に、これらのリストは空であり得る。というのも、IOライブラリは、目的に適ったデフォルトを提供するからである。
これら四つの要素と一緒になって、関数startIOは、一意な世界を変更する関数である。従って、その型は、*World -> *Worldである。
startIOの処理するイベントは、全てがプログラムの取得できる入力である。典型例は、ウィンドウ内のキーボードのキー、ウィンドウ内でのマウスクリックの押し放し又はマウス移動、メニューシステムの項目選択及び、タイマである。イベントは、あらゆるストリームでの出現順に処理される。関数startIOは、どのコールバック関数(イベントハンドラ)がこのイベント処理に指定されるのかを、指定されたデータ構造内のデバイス定義の中から探索する。このコールバック関数は、現在のプロセス状態に適用される。このプロセス状態のIOStコンポーネントは、プログラムの全デバイス定義を保持する。
イベントキューでのイベントの順序は、プログラムの動作を決める。このイベント順序は、プログラムの実際の入力である。オブジェクトI/Oシステムは、プロセス状態のI/O状態の中から、現在のイベントに対するハンドラを探す。このハンドラは、現在のプロセス状態に適用される。このことは、新しいプロセス状態を作り出す。このプロセスは、イベントの1つのハンドラがプログラム終了を指示するまで繰り返される。
従って、全てのコールバック関数は、新しいプロセス状態を出す。関数startIOは、その次のイベントに対応するコールバック関数に、この状態を引数として供給する。このことは、コールバック関数の1つがcloseProcessをプロセス状態に適用するまで継続する。この関数は、現在開いているデバイスを全て閉じ、結果として出される世界環境に未処理イベントを保存しておく。適切なハンドラを持たないイベントは無視される。
関数startIOは、CLEANのI/Oライブラリに定義済みである。以下の単純化されたバージョンは、どのようにしてイベント処理がなされるのかを例解している。警告:これはイベント処理を説明する為の仮のコードである。
startIO :: !DocumentInterface !.ps !(ProcessInit (PSt .ps)) ![ProcessAttribute (PSt .ps)]
!*World -> *World
startIO mdi public initialHandlers attributes world
# (events,world) = openEventQueue world
ps = {ls=public,io=createIOSt events}
ps = seq initialHandlers ps
{io} = DoIO ps
= closeEventQueue is.events world
where
DoIO ps=:{io}
| signalsQuit io = ps
# (newEvent,io) = getNextEvent io
(f,io) = findhandler newEvent io
ps = f {ps & io=io}
= DoIO ps
これは、まず最初に初期I/O演算が実行されるということを示している(initialHandlers psによってなされる)。初期I/O演算が終わると、イベントキューに現れた順にイベントが処理される。使用されるイベントハンドラは、プロセス状態のI/O状態コンポーネントの中に記録されている現在のデバイスの集合によって、決められる。そのI/O状態は引数の一部であり、イベントハンドラの結果である。このことは、イベントハンドラが、キューの末尾に、イベント処理の新しいコールバック関数を設置できるということを示している。プログラムは、適切な初期動作のリストによって関数startIOを呼出すだけで良い。この関数は、プログラムの動作全体を制御する。
ウィンドウシステムのプログラミングには、対応する用語(メニュー、ポップアップメニュー、モーダルダイアログ、ラジオボタン、閉じるボックス等)に関するいくらかの知識が必要である。そのような知識は、(そのようなシステムのユーザーとしての)読者にはあるものと仮定する。しかし、適当だと思われる場合には、この用語のいくつかを、具体例の中で出てきた場合に説明する。
5.4 ダイアログ
ダイアログは、高度に構造化された形式のウィンドウである。構造化された入力を提供する為に、ダイアログはコントロールを保持している。コントロールは、一般には標準の定義済みGUI要素だが、プログラマが定義することもできる。標準コントロールの例は、プログラムによって決められたテキストフィールド(TextControl)、ユーザー入力テキストフィールド(EditControl)とボタン(ButtonControl)である。コントロールは、既存のコントロールを組み合わせることでも定義できる。この方法で、より複雑な組合せを作ることが容易である。最も一般的なコントロールを保持するダイアログを図5.3に描く。
図5.3 コントロールの集合を保持するダイアログの例

ダイアログは2つのモードで開くことができる。
モードレスダイアログ: これは、ダイアログを開く一般モードである。モードレスダイアログは、関数openDialogを使用して開かれる。ダイアログは、関数closeWindowを使用してプログラムが閉じるまで開いたままである。その間には、プログラムは他のことができる。
モーダルダイアログ: プログラムが継続できる前に、ユーザーにダイアログを完全に処理させることが必要な場合が時々ある。この為には、モーダルダイアログは有用である。モーダルダイアログは、背景に押しやることのできないダイアログである。それは関数openModalDialogを使って開く。この関数は、そのダイアログが閉められる場合にのみ終了する。
ダイアログの従来からの使用法は、ユーザーが、編集コントロールのいくつかを埋め、ポップアップ、ラジオ又はチェックコントロールで適当な選択をし、ボタンコントロールの一つを押すことで動作を確認するということを(親切に)求められるということである。前に説明したように、ボタンコントロールを押すことは、関連付けられているイベントハンドラの評価を引き起こす。この関数は、ユーザーが記入したことは何であるのかと、何の選択がなされたのかを知らなければならない。この為に、それは、ダイアログの全情報を保持する抽象データ型であるWStateを検索することができる。テキストフィールドの値や選択されたラジオボタン若しくはチェックボックスは、適当な操作関数を使ってこのデータ型から抽出することができる。本章では、多くの例を示す。完全な定義は、StdControlに見ることができる。
5.4.1 Hello Worldダイアログ
ダイアログを使ったプログラムの最初の例として、もう1つのhello worldのバリアントを使用する。生成されるこのダイアログは、ユーザーに対するテキストメッセージと1つのボタンしか持たない。MacintoshとWindows PCでは、各々以下のように見える。
図5.4 hello worldダイアログ

加えて、プログラムはシングルメニューも持っている。このプルダウンのメニューは、終了コマンドのみを持っている。この全てに対するCLEANコードは、以下のようなものである。
module helloDialog
import StdEnv, StdIO
Start :: *World -> *World
Start world
= startIO MDI NilLS (open_dialog o openmenu) [] world
where
open_dialog ps
# (okId,ps) = openId ps
= snd (openDialog NilLS (dialog okId) ps)
dialog okId = Dialog "Hello Dialog"
( TextControl "Hello World, I am a Clean user!" [ ]
:+: ButtonControl "OK" [ ControlPos (Left,zero)
, ControlId okId
, ControlFunction quit ]
) [ WindowOk okId ]
openmenu = snd o openMenu NilLS filemenu
filemenu = Menu "File"
( MenuItem "Quit" [ MenuShortKey 'Q'
, MenuFunction quit ]
) []
quit :: (.ls,PSt .l) -> (.ls,PSt .l)
quit (ls,ps) = (ls,closeProcess ps)
(TextControl,ButtonControlのような)適当なデータ構成子は、ダイアログ内の各コントロールを決める。コントロールの最後の引数は、常にコントロール属性のリストである。また、I/Oライブラリは、指定される属性の量を限定する為に、これらの属性に意味のあるデフォルトを提供する。
Dialogの引数は、そのタイトル("Hello Dialog")、コントロールの構成(ここでは、1つのテキストコントロールと1つのボタンコントロール)とウィンドウ属性のリスト(デフォルトボタンのID)である。ダイアログのサイズは、属性によって上書きされるまで、そのコントロールのサイズによって決められる。あらゆるデバイスインスタンスは、そのデバイスインスタンスに対してのみ関係のあるデータを保存できる局所状態を持つ。その局所状態の初期値は、あらゆるデバイスオープン関数の第一引数として与えられる。この例では、ダイアログは非常のシンプルなので、ただ1つの型構成子NilLSが初期値として使用される。型NilLSは、局所状態を持たないデバイスのデフォルト状態として、StdIOに定義されている。[]又はundefのような他の値でも、等しく良く動作するだろう(注1。
IOプロセスの大域状態は、Pstと名付けられたレコードによって、StdIO内に定義されている。各レコードは、いくつかのプロセス依存の「局所」データ及び、そのIO環境を持つ。このIO環境は、抽象データ型IOStによってモデル化される。
:: *PSt l
= { ls :: !l // プロセスの局所(及びプライベート)データ
, io :: !*IOSt l // プロセスのIOSt環境
}
全てのデバイスインスタンスは大域状態と局所状態を持つという事実は、イベントハンドラの型の中にも見ることができる。あらゆるイベントハンドラは、型(.ls,PSt .ps) -> (ls.,PSt .ps)の状態遷移関数である。各ハンドラは、デバイスの局所状態とIOプロセスの局所状態を持つレコードを受け取り、それを作り出す。プロセスの局所状態は、デバイスの観点からしばしば大域状態と呼ばれる。これは、quitがその与えられた型を持つ理由である。多くのデバイスハンドラは空の局所状態を持つので、IOライブラリは、引数から結果にその局所状態を渡すだけの関数noLSを持つ。
noLS :: (.a ->.b) (.c,.a) -> (.c,.b)
noLS f (c,a) = (c,f a)
これによって、未変更の局所状態が関数noLSによって完全に処理されるイベントハンドラを書くことができる。noLSを使用すると、関数quitは、以下のように書くことができる。
quit :: ((.ls,PSt .l) ->(.ls,PSt .l))
quit = noLS closeProcess
このプログラムは、filemenuの定義するただ一つのメニューを開く。それの定義は、Menu型のインスタンスである。それの引数は、そのタイトル("File")、メニュー要素(メニューアイテム"Quit")とメニュー属性の空リストである。その唯一のメニュー要素は、型MenuItemのインスタンスである。それの引数は、その名前("Quit")と属性リスト(キーボートショートカット'Q'やダイアログのボタンと同一のコールバック関数)のリストである。OKボタンのコールバック関数も、quit関数を作動させ、プログラムを停止する。
メニューの名前とメニューアイテムの名前に&文字を加えることによるQuitMenuの定義である。Windowsマシンでは、このサインに続くこれらの文字は、メニューを通して(Altキーを押すことで)、キーボードナビゲーションに使用される。他のプラットフォームでは、これらの文字は無視される。
QuitMenu :: Menu MenuItem .p (PSt .l)
QuitMenu = Menu "&File"
( MenuItem "&Quit" [ MenuShortKey 'Q'
, MenuFunction noLS closeProcess
]
) []
openDialogやopenMenuのような全てのデバイス生成関数は、引数として、プロセス状態を取り、あり得るエラーメッセージと新しいプロセス状態を持つ組を出力する。
openMenu :: .ls !(mdef .ls (PSt .l)) !(PSt .l) -> (!ErrorReport, !PSt .l)
openDialog :: .ls !(wdef .ls (PSt .l)) !(PSt .l) -> (!ErrorReport, !PSt .l)
ここでは、エラー報告を無視し、関数sndによって、新しいプロセス状態を選択する。
デバイス定義の各部分は、そのId(そのデバイスの部分を特定する抽象型)によって指定できる。これらを使用すると、Idのダイアログを開閉し、ウィンドウを書き、ダイアログ内に書かれているテキストを読込み、メニューアイテムを無効化する等々が可能である。しかし、idは、デバイスで特別な動作をしたい場合にのみ必要とされる。他の全ての状況では、idの値は無関係であり、省略することができる。本例では、(WindowOK属性を使用して)OKボタンをダイアログのデフォルトボタンにする為に、そのボタンのidのみを必要とする。StdIOの一部であるStdIdのクラスIdsは、Idを定義する。このクラスは以下の関数を持つ。
openId :: !*env -> (!Id, !*env)
openIds :: !Int !*env -> (![Id], !*env)
これらの関数は、1つのId又はn個のIdリストを生成する。この環境は、World、IOSt又はPStのどれでも良い。
デバイスのIdを可能な限り局所的にしたままにすることが、勧められるプログラミングスタイルである。上のプログラムは、ダイアログ定義内にIdを生成する。分かっている数のIdを必要とする場合には、関数OpenIdsが有用である。それは、Idのリストを返し、そのリストの要素には、より多くのIdが必要である場合に容易に拡張されるワイルドカードを持つリストパターンを使用して、アクセスできる。
対話型プログラムを書く場合には、必要とされるモジュールをインポートすることを忘れてはならない。UnixやLinuxシステム上ではまた、ウィンドウベースのプログラムに対する適切なライブラリをリンクすべきである。貴方のCLEANディストリビューションの文書を参照。
なによりもまず、そのような比較的シンプルなプログラムを多く説明しなければならない。幸運なことに、同一の原則は、IOライブラリ全体を通して適用される。ここで得た知識は、多くの他のIOプログラムを理解し開発するのを助けてくれるだろう。最後に、このことは、これらの種類のインターフェースをプログラムするのに非常に簡潔でプラットフォーム非依存な方法であることに留意することは価値のあることである。プレーンなCライブラリを使用すると、そのようなプログラムは、大抵はマグニチュード単位で大きくなるので、単一のプラットフォームだけにしか適合しないのである。
注1)startIOは、大域状態において正格なので、値undefは、使用されていない大域状態に対する値としては不適合である。
5.4.2 ファイルコピーダイアログ
いま、ユーザーに以下のダイアログ(図5.5参照)を示してファイルコピーするプログラムの対話型バージョンを作りたいものとする。
図5.5 関数CopyFileDialogのダイアログの結果

ファイルコピープログラムの場合にも、プログラム状態はNilLSで良い。対話型ファイルコピープログラムは、上のhello worldの例と類似の構造を持つ。
import StdEnv, StdIO
nrlines :== 1
lengthOfFileName :== (PixelWidth 150)
Start :: *World -> *World
Start world = CopyFileDialogInWorld world
CopyFileDialogInWorld :: *World -> *World
CopyFileDialogInWorld world
= startIO NDI NilLS opendialog [] world
where
opendialog ps
# ([dlgId,okId,srcId,dstId:_],ps) = openIds 4 ps
# copyFileDialog
= Dialog "File Copy"
( LayoutControl
( TextControl "File to read: " []
:+: TextControl "Copied file name: " [ ControlPos (Left,zero) ]
) [ ControlHMargin 0 0
, ControlVMargin 0 0 ]
:+: LayoutControl
( EditControl "" lengthOfFileName nrlines [ ControlId srcId ]
:+: EditControl "" lengthOfFileName nrlines [ ControlId dstId
, ControlPos (Left,zero) ]
) [ ControlHMargin 0 0
, ControlVMargin 0 0 ]
:+: ButtonControl "Cancel" [ ControlPos (Left,zero)
, ControlFunction quit ] // quitは5.4.1参照
:+: ButtonControl "OK" [ ControlId okId
, ControlFunction (noLS (ok dlgId srcId dstId))]
) [ WindowId dlgId
, WindowOk okId ]
= snd (openDialog NilLS copyFileDialog ps)
ok id sid did ps
# (Just wstate,ps) = accPIO (getWindow id) ps
[(_,Just inputfilename),(_,Just outputfilename):_]
= getControlTexts [sid,did] wstate
# ps = appFiles (CopyFile inputfilename outputfilename) ps
= closeProcess ps
このプログラムは、No Document Interfaceを使用している。これは、ダイアログだけがユーザーとの対話に利用できるということを意味している。本例では、それこそが我々の求めているものである。ダイアログの外見(図5.5参照)は、関数copyFileDialogによって決められる。ダイアログ定義それ自体は、再び、そのコンポーネントの列挙からなる。ダイアログ関数は、上のhello-worldの例のダイアログに似ている。興味深い追加は、我々が2つのきちんとした行を得る為に、テキストコントロールと編集コントロールをグループ化するのにレイアウトコントロールを使用しているということである。コントロールをグループ化するこの軽量のレイアウトコントロールとは別に、コントロールは、合成コントロールの中にグループ化することもできる。合成コントロールは、コントロールを保持する部分ウィンドウを生成し、これは、いくつかの状況での少し異なった動作を示し、非効率である。
実際にファイルをコピーするコードは、上に導入したコードと同一である。
上に定義したダイアログの欠点は、それによってユーザーが、ファイルシステムをブラウズし、コピーされるべきファイルを探索することができないということである。ライブラリモジュールStdFileSelectの関数を使用すると、そのようなダイアログが、プログラムが動作する実際のマシンの標準的な方法で生成される。
import StdFileSelect
fileReadDialog :: (String (PSt .l) -> PSt .l) (PSt .l) -> PSt .l
fileReadDialog fun ps
# (maybe_name,ps) = selectInputFile ps
| isJust maybe_name = fun (fromJust maybe_name) ps
| otherwise = ps
fileWriteDialog :: (String (PSt .l) -> PSt .l) (PSt .l) -> PSt .l
fileWriteDialog fun ps
# (maybe_name,ps) = selectOutputFile prompt defaultFile ps
| isJust maybe_name = fun (fromJust maybe_name) ps
| otherwise = ps
where
prompt = "Write output as:"
defaultFile = "file.copy"
図5.6 MacとWindowsの標準的なselectInputFileダイアログ

5.4.3 関数テストダイアログ
関数GreatFunを書いて、それをある入力値でテストしたいとする。これを行う方法は、コンソールモードを使用して、右手側に、様々な入力値を持つ関数適用の組又はリストを持つstart規則を導入することである。
Start = map GreatFun [1..1000]
又は、例えば、
Start = (GreatFun 'a', GreatFun 1, GreatFun "GreatFun")
実際には、この静的なテスト方法では、動的対話型テストに比べて、テストに際しての多様性が発生しないということが分かる。対話型テストの為には、入力値を打ち込めるダイアログが非常に役に立つ。
前セクションではダイアログの定義方法を示した。ここでは、引数に関数のリストを取り、その関数をテストできるダイアログを持つ対話型プログラムを作り出す関数を定義する。我々はこの定義が非常に汎用であることを望んでいる。(Stringとしてダイアログに打ち込まれる)入力値を、テスト関数の必要な引数に変換できることが必要なため、多重定義を使用する。
多重定義されたテストダイアログは、構造化された引数(リスト、木、レコード...)に作用する関数を簡単にテストするのに使用できる。必要とされることは、型又は部分型に対するfromStringとtoStringのインスタンスがまだ利用可能でない場合にそれらを書くことだけである。
関数テストダイアログは、テストされる関数を持つプログラムにインポートされるべきモジュールとして組織される。このインポートされるモジュールは、関数引数の文字列から変換したり、関数結果の文字列に変換したりする適切なダイアログと多重定義関数を生成する関数を保持している。各関数テストダイアログに対しては、ダイアログを動作させるメニューFunctionsに、メニュー要素がある。このメニュー要素は、通常のように:+:によって構成されるのではなく、u listのように、ListLsによって構成される。その定義モジュールは以下のようである。
implementation module funtest
import StdEnv,StdIO
functionTest :: [(([String] -> String),[String],String)] *World -> *World
functionTest [] world = world
functionTest funs world
# (ids,world) = openIds (length funs) world
= startIO MDI Void (initialIO funs ids) [] world
initialIO funs dialogIds = openfunmenu o openfilemenu
where
openfilemenu = snd o openMenu Void fileMenu
openfunmenu = snd o openMenu Void funMenu
fileMenu = Menu "&File"
( MenuItem "&Quit" [ MenuShortKey 'Q'
, MenuFunction (noLS closeProcess)
]
) []
funMenu = Menu "Fu&nctions"
(ListLS
[ MenuItem fname [ MenuFunction (noLS opentest)
: if (c<='9') [MenuShortKey c] []
]
\\ (_,_,fname) <- funs
& opentest <- opentests
& c <- ['1'..]
]
) []
opentests = [functiondialog id fun \\ fun <- funs & id <- dialogIds]
ダイアログを定義する関数(functiondialog)を以下に定義する。これはファイルコピーダイアログに非常に似ている。まず、関数引数のフィールドリストが生成される。各引数には、その引数を指示するテキストコントロールと、その実引数値を持つ編集コントロールが存在する。
結果と3つのボタンのフィールドによって、ダイアログを完成させる。最初のボタンはこのダイアログを閉じる。第2のボタンはプログラムを終了する。最後のボタンはデフォルトのボタンである。このボタンは、テストされる関数を評価する。実引数は、ダイアログの編集フィールドから抽出される。これらの引数は、文字列のリストとして、テストされる関数に渡される。close関数は多種のダイアログに対して一般的に使用できることに注意して欲しい。
functiondialog :: Id (([String]->String),[String],String) (PSt .ps) -> PSt .ps
functiondialog dlgId (fun,initvals,name) ps
# (argIds, ps) = accPIO (openIds arity) ps
(resultId,ps) = accPIO openId ps
(evalId, ps) = accPIO openId ps
= snd (openDialog undef (dialog argIds resultId evalId) ps)
where
dialog argIds resultId evalId
= Dialog name
(ListLS
[ TextControl (arg_label n)
[ ControlPos (LeftOf (argIds!!n), zero)
, ControlWidth (ContentWidth ("arg " +++ toString arity))
]
:+: EditControl val width nrlines
[ ControlId (argIds!!n)
: if (n==0) [] [ControlPos (Right,zero)]
[ControlPos (Below (argIds!!(n-1)),zero)]
]
\\ val <- initvals
& n <- [0..]
]
:+: TextControl "result" (if (arity==0) [ControlPos (Left,zero)]
[ControlPos (LeftOf resultId,zero)])
:+: EditControl "" width nrlines
[ ControlId resultId
: if (arity==0) [] [ControlPos (Below (argIds!!(arity-1)),zero)]
]
:+: LayoutControl
( ButtonControl "Close"
[ ControlFunction (noLS (closeWindow dlgId)) ]
:+: ButtonControl "Quit"
[ ControlFunction (noLS closeProcess) ]
:+: ButtonControl "Eval"
[ ControlId evalId
, ControlFunction (eval dlgId argIds resultId fun)]
) [ ControlPos (Center,zero)]
) [ WindowId dlgId
, WindowOk evalId ]
arity = length initvals
eval id argIds resultId fun (ls,ps)
# (Just wstate,ps) = accPIO (getWindow id) ps
input = [fromJust arg \\ (_,arg)<-getControlTexts argIds wstate]
= (ls,appPIO (setControlText resultId (fun input)) ps)
arg_label n = "arg " +++ toString n
テストされる関数の引数は、ダイアログからの文字列リストとして集められる。以下の関数は、適切な型変換を行うのに使用できる。型クラスを使用することで、これらの関数は非常に汎用になりうる。関数を処理する類似の関数に別の引数の数を加えると、非常にシンプルである。
no_arg :: y [String] -> String | toString y
no_arg f [] = toString f
no_arg f l = "This function should have no arguments instead of "+++toString (length l)
one_arg :: (x -> y) [String] -> String | fromString x & toString y
one_arg f [x] = toString (f (fromString x))
one_arg f l = "This function should have 1 argument instead of "+++toString (length l)
two_arg :: (x y -> z) [String] -> String | fromString x & fromString y & toString z
two_arg f [x,y] = toString (f (fromString x) (fromString y))
two_arg f l = "This function should have 2 arguments instead of "+++toString (length l)
three_arg :: (x y z -> w) [String] -> String
| fromString x & fromString y & fromString z & toString w
three_arg f [x,y,z] = toString (f (fromString x) (fromString y) (fromString z))
three_arg f l = "This function have 3 arguments instead of "+++toString (length l)
このモジュールを完成するにはいくつかの定数を定義すれば良い。
nrlines :== 2
width :== PixelWidth 250
関数sqrt、powerと"Hello world"をテストするために、以下のプログラムを書く。
module functiontest
import funtest
Start world = functionTest funs world
funs = [ (one_arg squareRoot ,["2"] ,"squareRoot")
, (two_arg power ,["2","10"] ,"power")
, (no_arg "Hello world" ,[] ,"Program")
]
power x 0 = 1
power x n
| isEven n
# y = power x (n/2)
= y * y
= x * power x (n-1)
squareRoot :: Real -> Real
squareRoot r = sqrt r
このプログラムを実行し、全てのダイアログを開く場合、次の図で示されるようなインターフェースを得る。このことで、ユーザーが関数を対話的にテストすることが可能になる。
図5.7 関数テストダイアログシステムジェネレータの使用例

これは、多相型関数をテストする一般的なダイアログの完全な定義をすでに完成させている。ユーザー定義データ構造(リスト、木、レコード)に対するテストダイアログを書くことは簡単である(実際、必要とされるものはfromStringとtoStringのインスタンスを書くことのみである)。例えば、
instance fromString Int
where fromString i = toInt i
instance fromString Bool
where
fromString "True" = True
fromString "False" = False
instance fromString Real
where fromString r = toReal r
このプログラムの唯一の問題は、汎化することである。多重定義関数がテストされる場合、内部多重定義を常に解決できるわけではない。多重定義型ではなく、制限された(多相)型を持つバージョンを定義することで、この問題は通常の方法で解決される。このことを、前に関数sqrtで見た。
5.4.4 メニュー関数に対する入力ダイアログ
同様に、メニュー関数の入力ダイアログを定義できる。メニュー関数は、MenuFunction属性を持つ引数関数である。これの型は、標準プロセス状態遷移関数IdFun (PSt .l)である。これは、前例のopen、test sinやquitのようなメニュー関数の型である。
ダイアログではほんのわずかしか変更されてはならない。ダイアログ生成関数の型は以下のようになる。
menufunctiondialog :: Id (([String] -> IdFun (PSt .l)),[String],String) (PSt .l) -> PSt .l
更に、ダイアログから結果コントロールを除去する。メニュー関数の結果は、印刷可能である。局所eval関数は、以下のものになるはずである。
eval id argIds resultId fun (ls,ps)
# (Just wstate,ps) = accPIO (getWindow id) ps
input = [fromJust arg\\(_,arg)<-getControlTexts argIds wstate]
= (ls,fun input ps)
この入力ダイアログは、単一の(構造化された)入力を必要とする全種のメニューに使用できる。関数inputdialogを名前、幅及びメニュー関数に適用した結果もまた、余分な入力を組み入れるメニュー関数である!
5.4.5 汎用通知
通知は、少なくとも1つのボタンが続く、多くのテキスト行を持つシンプルなダイアログである。このボタンは、多くの選択肢をユーザーに示す。ある選択肢を選択すると、この通知を閉じるので、プログラムは自己の作業を継続することができる。ここに型定義を示す。
:: Notice ls ps = Notice [String] (NoticeButton *(ls,ps)) [NoticeButton *(ls,ps)]
:: NoticeButton ps = NoticeButton String (IdFun ps)
Dialogs型構成子クラスの新しいインスタンスを通知にしようと思う。そこで、多重定義関数openDialog、openModalDialogとgetDialogTypeの実装を提供しなければならない。また、局所状態に興味がない場合に通知を開く便利な関数openNoticeも加える。
instance Dialogs Notice where
openDialog :: .ls (Notice .ls (PSt .ps)) (PSt .ps) -> (ErrorReport,PSt .ps)
openDialog ls notice ps
# (wId, ps) = accPIO openId ps
(okId,ps) = accPIO openId ps
= openDialog ls (noticeToDialog wId okId notice) ps
openModalDialog :: .ls (Notice .ls (PSt .ps)) (PSt .ps) -> ((ErrorReport,Maybe .ls),PSt .ps)
openModalDialog ls Notice ps
# (wId, ps) = accPIO openId ps
(okId,ps) = accPIO openId ps
= openModalDialog ls (noticeToDialog wId okId notice) ps
getDialogType :: (Notice .ls .ps) -> WindowType
getDialogType notice = "Notice"
openNotice :: (Notice .ls (PSt .ps)) (PSt .ps) -> PSt .ps
openNotice notice ps = snd (openModalDialog undef notice ps)
関数noticeToDialogは、通知をダイアログに変換する。この関数は、便利なことに、リストの内包表記や合成コントロールを使用して、このレイアウトを制御する。ここにその定義を示す。
noticeToDialog :: Id Id (Notice .ls (PSt .ps))
-> Dialog (:+: (LayoutControl (ListLS TextControl))
(:+: ButtonControl
(ListLS ButtonControl)
)) .ls (PSt .ps)
noticeToDialog wId okId (Notice texts (NoticeButton text f) buttons)
= Dialog ""
( LayoutControl
( ListLS
[ TextControl text [ControlPos (Left,zero)] \\ text <- texts ]
) [ ControlHMargin 0 0
, ControlVMargin 0 0
, ControlItemSpace 3 3
]
:+: ButtonControl text
[ ControlFunction (noticefun f)
, ControlPos (Right,zero)
, ControlId okId
]
:+: ListLS
[ ButtonControl text
[ ControlFunction (noticefun f)
, ControlPos (LeftOfPrev,zero)
]
\\ (NoticeButton text f) <- buttons
]
) [ WindowId wId
, WindowOk okId
]
where
noticefun f (ls,ps) = f (ls,closeWindow wId ps)
新しいモジュールnoticeに、Dialogs型構成子クラスのこの新しいインスタンスをエクスポートできる。
definition module notice
import StdWindow
:: Notice ls ps = Notice [TextLine] (NoticeButton *(ls,ps)) [NoticeButton *(ls,ps)]
:: NoticeButton ps = NoticeButton TextLine (IdFun ps)
instance Dialogs Notice
openNotice :: (Notice .ls (PSt .ps)) (PSt .ps) -> PSt .ps
この通知実装が与えられると、以下の例の中でこれを使用できる。これらは自己説明的である。
import StdEnv, StdIO, notice
// 適用される関数について警告:デフォルトはcancel
warnCancel :: [a] .(IdFun (PSt .ps)) (PSt .ps) -> PSt .ps | toString a
warnCancel info fun ps = openNotice warningdef ps
where
warningdef = Notice (map toString info) (NoticeButton "Cancel" id)
[NoticeButton "OK" (noLS fun)]
// 適用される関数について警告:デフォルトはOK
warnOK :: [a] (IdFun (PSt .ps)) (PSt .ps) -> PSt .ps | toString a
warnOK info fun ps = openNotice warningdef ps
where
warningdef = Notice (map toString info) (NoticeButton "OK" (noLS fun))
[NoticeButton "Cancel" id]
// ユーザーへのメッセージ: 続けるならOK
inform :: [String] (PSt .ps) -> PSt .ps
inform strings ps = openNotice (Notice strings (NoticeButton "OK" id) []) ps
上の関数は、このプログラムのユーザーに告知及び警告するだけではなく、特定の関数が呼出される場合に、プログラマに引数及び(部分)構造の情報を提供するのにも使用できる。後者は、プログラムのデバッグを行う場合に非常に有用であり得る。
図5.8 上で定義した通知のいくつかの簡単なアプリケーション

通知を生成するこれら汎用の関数は、以下のプログラム例で使用される。
5.4.6 状態を持つダイアログ
今までは、状態を持たないダイアログのみを見てきた。本セクションでは、カウンタを実装するダイアログを示す。カウンタの値は、ダイアログの局所状態に保存される。プロセス全体の状態は、依然として空Voidである。プログラムは、上に示したダイアログに非常に似ている。
module counter
import StdEnv, StdIO
Start :: *World -> *World
Start world = startIO NDI Void initIO [] world
initIO pst
# (id,pst) = openId pst
= snd (openDialog 0 (dialog id) pst)
where
dialog textid
= Dialog "Counter"
( TextControl "Counter value" []
:+: TextControl "0 " [ ControlId textid
]
:+: ButtonControl "&-" [ ControlFunction (upd (\n -> n-1))
, ControlPos (Left,zero)
]
:+: ButtonControl "&0" [ ControlFunction (upd (\n -> 0))
, ControlTip "Set counter to 0"
]
:+: ButtonControl "&+" [ ControlFunction (upd (\n -> n+1))
]
) [ WindowClose (noLS closeProcess)
]
where
upd :: (Int->Int) (Int,PSt .l) -> (Int,PSt .l)
upd f (count,pst)
# count = f count
= (count, appPIO (setControlText textid (fromInt count)) pst)
局所状態の実際の変更は、制御関数updによって行われる。この関数は、実際の更新を行う関数によってパラメータ化される。この新しい状態が出力されると、これに適切なテキストコントロールは、この局所状態の新しい値に従って更新される。得られたダイアログは、以下のように見える。
図5.9 カウンタダイアログ
本プログラムの2、3の詳細は、論ずるに当たらない。カウンタの値を示すテキストコントロールの最初のテキストは、大きなカウンタ値に対して十分広くする為に、一連のスペースを持つ。ボタンは、&文字を使用したキーボードインターフェースを備える。真中のボタンは、(上の図で示されるように)ツール情報を持つ。
5.5 ウィンドウ
ウィンドウをプログラムすることは、ダイアログをプログラムするより精密である(ダイアログは、より多くの構造を持っているので、ライブラリがその仕事の大部分を処理できる)。ダイアログは基本的に、固定サイズのフレーム内にあるコントロールの集合である一方で、ウィンドウの目的は、キーボード及びマウスを経由して、ユーザーが操作できる文書を表示することである。一般に、ウィンドウは、文書の一部のみを表示するので、ユーザーは、スクロールしたり、ウィンドウのサイズを変更することが認められている。結果として、ウィンドウは、必要とされる場合(例えば、ウィンドウが別のウィンドウの前にきた場合又はスクロールした場合)にウィンドウ(の一部)を再描画する更新関数(update function)を持たなければならない。ウィンドウは、コントロールの集合たるダイアログと共通しており、その設置も同じように柔軟である。
図5.10 ウィンドウの各名称

ウィンドウ内に表示される文書は、ウィンドウの背景に示される。これは、「文書層(document layer)」と呼ばれる。もしウィンドウ内にコントロールがあるならば、これらは、その文書の前、つまり、「制御層(control layer)に置かれる。これらの層は、ウィンドウ(ダイアログ)フレーム(window (dialog) frame)内部に視覚的に留められる。
その文書をユーザーに表示する為には、プログラムは、文書層に描画しなければならない。この為に、文書層は、ピクチャを持つ。ピクチャは、型*Pictureの、一意に属性付けられた環境である。モジュールStdPictureは、ピクチャに作用する全ての描画演算を持つ。各描画演算は、そのピクチャに効果を持つ。ピクチャの最小の描画単位は、ピクセル(pixel)である。ピクセルは、その座標によって特定され、その座標には、Point2データ型を使用する。Point2は、整数の対である。
:: Point2 = { x :: !Int
, y :: !Int
}
instance == Point2
instance + Point2
instance - Point2
instance zero Point2
ピクセルの座標は、左から右、トップから底へと増加する。最初の整数は、水平位置を決める。しばしばx座標と呼ばれる。もう1つの整数は通常は、y座標と呼ばれ、頂上から底辺まで移動する場合に増加するということを忘れないで欲しい。これは、数学の慣習とは違う!ピクチャは、有限の領域を持つ。つまり、x座標とy座標の範囲は、[-230, 230]である。これはかなり大きい。より小さいビュー領域(view domain)を洗濯することで、その範囲をより小さくすることができる。幅zero(又は高さzero)のビュー領域を選択することは認められていない。
ウィンドウによって、ユーザーは、文書層をスクロールし、ピクチャの様々なセクションを表示することが認められる。ウィンドウの左上端に表示されるピクセルの座標は、ウィンドウ原点(window origin)と呼ばれる。ピクチャの可視的部分は、ウィンドウビューサイズ(window view size)によって決められる。従って、現在のウィンドウビューサイズが、wピクセルの幅と、hピクセルの高さを持ち、現在のウィンドウ原点が点(x,y)であるならば、xとx+wの間のx座標と、yとy+hの間のy座標を持つ全てのピクセルは、原則として可視的である。
オブジェクトI/Oライブラリは、ウィンドウのスクロール、ズーム及びリサイズを処理する。Windowsマシンでは、システムボタンと最小化ボタンも、システムによって提供される。マウスイベント、キーボードイベントや閉じるボックスのクリックに関連付けられる動作は、プログラムが決める。プログラムは常に、ピクチャの座標システム内で動作している。プログラムが現在のウィンドウ内にはないピクチャ領域の一部で何かを描画する場合、その描画は、評価されるだろうが、視覚的な効果を持たない。描画をスピードアップする為に、現在のウィンドウ内部のアイテムだけを表示できるように、描画関数を定義することができる。これは、描画が(余りに)時間浪費的である場合にのみ価値がある。
現在ウィンドウの外側にあり、ある他のウィンドウ又はコントロールの背後に隠されているウィンドウの一部は、オブジェクトI/Oライブラリによって記憶されてはいない。ユーザーがウィンドウをスクロールするか、他のものの前にあるウィンドウを移動する時にはいつでも、新しく表示されるピクチャ領域を描画する必要がある。オブジェクトI/Oシステムは、プログラムがオプションとして提供する関数を使用する。この関数は、いわゆるルック(look)関数である。これは、以下の型を持つ。
:: Look :== SelectState -> UpdateState -> *Picture -> *Picture
:: UpdateState = { oldFrame :: !ViewFrame
, newFrame :: !ViewFrame
, updArea :: !UpdateArea
}
:: ViewFrame :== Rectangle
:: UpdateArea :== [ViewFrame]
この更新関数は、引数として、現在のウィンドウのSelectStateと、型UpdateStateのレコードで定義する更新されるべき領域の記述を持つ。このレコードは、更新される矩形のリスト(updAreaフィールド)と、ウィンドウの現在の可視的部分(newFrameフィールド)を保持する。ウィンドウのサイズ変更により更新が生成された場合には、ウィンドウの前のサイズも与えられる(oldFrameフィールド)。このフィールドは、ウィンドウがリサイズされない場合にはnewFrameフィールドに等しい。
5.5.1 ウィンドウでHello World
セクション5.4.1では、ダイアログを使ったhello worldプログラムの生成方法を示した。ここでは、メッセージをウィンドウに入れることによって、このプログラムのよりエキサイティングなバージョンを生成する方法を示す。"Hello World!"メッセージは、look関数(WindowLook属性)によって描画される。このウィンドウは、(Windowの第2引数としてNilLSを使うことによって表現される)コントロールを持たない。我々は、ビュー領域を設定しない。この場合には、オブジェクトI/Oシステムは、0と230の間のデフォルトの座標値を選択する。初期ウィンドウビューサイズを設定することによって、良くサイズ化されたウィンドウを得る。もしこの属性を省略すると、オブジェクトI/Oシステムは、できる限り大きく、しかも、与えられたウィンドウビュー領域の内側のウィンドウを生成する(これは通常はスクリーンより大きいので、フルスクリーンウィンドウビューフレームを結果する)。このプログラムは、4つの方法で終了できる。つまり、FileメニューのQuitコマンドを選択することによって、ウィンドウがアクティブである場合に、キーボードのキーを押すことによって(WindowKeyboard属性)、ウィンドウ内でマウスを押すことによって(WindowMouse属性)、又は、ウィンドウを閉じることによって(WindowClose属性)である。
module helloWindow
import StdEnv, StdIO
Start :: *World -> *World
Start world = startIO SDI Void (openwindow o openmenu) [] world
where
openwindow = snd o openWindow Void window
window = Window "Hello window"
NilLS
[ WindowKeyboard filterKey Able (\_ -> quit)
, WindowMouse filterMouse Able (\_ -> quit)
, WindowClose quit
, WindowViewSize {w=160,h=100}
, WindowLook True (\_ _ -> look)
]
openmenu = snd o openMenu Void file
file = Menu "&File"
( MenuItem "&Quit" [MenuShortKey 'Q',MenuFunction quit]
) []
quit = noLS closeProcess
look = drawAt {x=30,y=30} "Hello World!"
filterKey key = getKeyboardStateKeyState key<>KeyUp
filterMouse mouse = getMouseStateButtonState mouse == ButtonDown
このプログラムは次の図に示されるようなウィンドウを作り出す。
図5.11 hello worldプログラムhello Windowのウィンドウ

5.5.2 ペアノ曲線
線がどのようにしてウィンドウ内部で描かれるかを示すために、ペアノ曲線を取り扱う。数に関する公理系とは別に、Giuseppe Peano(1858-1932)は、どのようにして正方形を覆い尽くす線を描くことができるかを研究した。これを行うシンプルな方法は、左から右に、しかも、規則的な距離で戻って、線を描くことである。更に興味深い曲線は、以下のアルゴリズムを使って取得できる。オーダー0は何もしないということである。最初のオーダーでは、左上の平面で始まり、右のほうにペンを移動し、下に下げ、左に移動する。これはペアノ曲線1である。ペンの基本的な動きは下がることであり、この曲線をsouthと呼ぶ。第2ペアノ曲線では、ペアノ1の各線を類似の図に置換える。左への線は南、東、北そして東に置換えられる。新しい線の各々は、前のオーダーでの線の半分に過ぎない。このプロセスを、追加した線に繰り返すことで、以下の一連のペアノ曲線を取得する。
図5.12 いくつかのペアノ曲線

図を生成するプログラムを書くためには、描画関数のリストを生成しなければならない。StdPictureとStdIOCommonから、以下の型を使用する。
:: Picture
class Drawables figure
where
draw :: !figure !*Picture -> *Picture
drawAt :: !Point2 !figure !*Picture -> *Picture
undraw :: !figure !*Picture -> *Picture
undrawAt :: !Point2 !figure !*Picture -> *Picture
instance Drawables Vector2
:: Vector2 = {vx::!Int,vy::!Int} // StdIOCommonで定義している
stdPenPos :: !Point2 !*Picture -> *Picture
詳細には、関数setPenPosを使用してペンをその引数の座標に移動し、多重定義Draw関数のVectorインスタンスを使用して、現在のペン位置から、与えられたベクタに沿って線を引き、新しいペン位置を取得する。
上の絵は、4つの相互再帰関数によって生成される。整数引数nは、その近似の数を決める。線の長さdは、ウィンドウサイズ及び使用した近似によって決められる。関数の各々の中に線リストを生成し、これらのリストを追加するのではなく、継続を使用する。一般に継続は、現在の関数が終了した場合に何がなされなければならないかを決める。この状況では、継続は、このペン移動の終了後に描画される線のリストを保持する。
module peano
peano :: Int -> [*Picture -> *Picture]
peano n = [ setPenPos {x=d/2,y=d/2}
: south n []
]
where
south 0 c = c
south n c = east (n-1) [ lineEast
: south (n-1) [ lineSouth
: south (n-1) [lineWest:west (n-1) c]
]
]
east 0 c = c
east n c = south (n-1) [ lineSouth
: east (n-1) [ lineEast
: east (n-1) [lineNorth: north (n-1) c]
]
]
north 0 c = c
north n c = west (n-1) [ lineWest
: north (n-1) [ lineNorth
: north (n-1) [lineEast: east (n-1) c]
]
]
west 0 c = c
west n c = north (n-1) [ lineNorth
: west (n-1) [ lineWest
: west (n-1) [lineSouth: south (n-1) c]
]
]
lineEast = draw {vx= d,vy= 0}
lineWest = draw {vx= ~d,vy= 0}
lineSouth = draw {vx= 0,vy= d}
lineNorth = draw {vx= 0,vy= ~d}
d = windowSize / (2^n)
プログラムに埋め込む
これらの曲線を描くのにウィンドウが必要である。これは、かなり標準的な方法で行う。ウィンドウは、2つのメニューも開くstartIOの適切な初期動作によって生成される。論理状態は必要ではない。ペアノ曲線の現在のオーダーは、look関数に黙示的に保存される。ウィンドウのlook属性を変更するつもりなので、それを特定しなければならない。この為に、まず、(モジュールStdIdに定義されている)関数openIdを使用して、型Idの識別値を生成し、それをstartIOの初期化関数に渡す。すると、initialIOの全ての局所関数定義は、この識別値を容易に参照できる。
import StdEnv, StdIO
Start :: *World -> *World
Start world
# (id,world) = openId world
= startIO SDI Void (initialIO id) [] world
where
initialIO wId
= seq [openwindow,openfilemenu,openfiguremenu]
2つのメニューが開かれる。。ファイルメニューはメニューアイテムquitのみを持つ。figureメニューは、様々なペアノ曲線を生成する項目を持つ。このメニューは、適切なリストの内包表記によって生成される。
openfilemenu = snd o openMenu undef file
file = Menu "&File"
( MenuItem "&Quit"
[ MenuShortKey &Q&
, MenuFunction (noLS closeProcess)
]
) []
openfiguremenu = snd o openMenu undef fig
fig = Menu "Fi&gure"
( ListLS
[ MenuItem (toString i)
[ MenuShortKey (toChar (i + toInt '0'))
, MenuFunction (noLS (changeFigure i))
]
\\ i <- [1..8]
]
) []
図を変更するには、3つの動作が必要である。第1に、描かれる現在のペアノ曲線を反映するため、setWindowTitleを使用してウィンドウタイトルを変更する。そして、ウィンドウlook関数は、setWindowLookを使用して新しい曲線に設定される。最後に、drawInWindowを使用して、ピクチャ領域全体が消され、appWindowPictureを使用して、新しい図が描かれる。
changeFigure peano_nr pst =:{io}
# io = setWindowTitle wId ("Peano " +++ toString peano_nr) io
# io = setWindowLook wId False (False, \_ _ -> seq figure) io
= {pst & io = appWindowPicture wId redraw io}
where
redraw = seq [ unfill pictDomain : figure ]
figure = peano peano_nr
このウィンドウはかなり標準的なスクロールウィンドウである。ほんの少し特別である唯一の点は、図の周りに白の余白を入れているという事実である。我々は、このことを、ウィンドウビュー領域属性を、zeroではなく、{x= ~margin, y=~ margin}に設定するということによって達成する。この方法では、zeroからペアノ曲線を描くことができ、オブジェクトI/Oシステムは、余白に気を払うだろう。WindowHScrollとWindowVScroll属性は各々、水平と垂直のスクロールバーを加える。これらの属性関数は、ユーザーがスクロールバーを使う場合にいつも評価される。この場合には、新しい親指の位置を計算しなければならない。この状況はかなり頻繁に起こるので、オブジェクトI/Oライブラリは、あなたが使用できる(モジュールStdIOCommonの)定義済み関数stdScrollFunctionを持つ。これは、方向とスクロールステップサイズでパラメータ化される。メニュー関数changeFigureは、ウィンドウがwIdのId値によって特定されると想定していることに注意して欲しい。もしWindowId属性を設定し忘れると、メニュー演算は、求めている効果を持つことはないだろう。
openwindow = snd o openWindow undef window
window
= Window
"Peano"
NilLS
[ WindowHScroll (stdScrollFunction Horizontal 10)
, WindowVScroll (stdScrollFunction Vertical 10)
, WindowViewDomain pictDomain
, WindowViewSize {w=windowSize+2*margin,h=windowSize+2*margin}
, WindowLook True (\_ _ -> seq (peano 1))
, WindowId wId
]
このプログラムを完全にする為には、いくつかの定数を定義しさえすれば良い(図5.12のイメージは、128のwindowSizeを使用して生成された)。
windowSize :== 512
pictDomain :== { corner1 = {x= ~margin, y= ~margin}
, corner2 = {x= windowSize+margin,y= windowSize+margin}
}
margin :== 4
メモリ使用
上に説明したプログラムは正しく動くが、多大な量のメモリを使用する(peano 8で数メガバイト)。このプログラムは、ペアノ曲線が非常に多数の線からなり、プログラムが将来の再利用の為にこの線のリスト全体を保持しているので、非常に多量のメモリを使用しているのである。
線の数をcを呼ぶ場合、計算量として、オーダーnのペアノ曲線の線の数は以下のものである。
c 0 = 3
c n = 3 + 4*c (n-1)
このことは、関数north、east、south及びwestの定義の構造から明らかである。次章では、このことから線の数がペアノ曲線のオーダーに対して指数的に増大することが分かると述べる。
c 1 = 3
c 2 = 15
c 3 = 63
c 4 = 255
c 5 = 1023
c 6 = 4095
c 7 = 16383
c 8 = 65535
c 9 = 262143
c 10 = 1048575
ピクチャ更新関数として表現されるこの巨大な量の線を記憶する理由は、関数changeFigureから明らかになる。値figureはペアノ曲線の線のリストである。それは、appWindowPictureの引数に渡すことによって計算され、ウィンドウを更新する。同じ引数が新しいウィンドウlook関数に使用されるので、計算結果は再利用の為に保存される。これは、CLEANが使用するグラフ簡約スキームの1例である。つまり、式はほとんど一回で評価される。通常はこれは利益であり、簡約結果の共有により、プログラムはより速くなる。
この状況では、メガバイトのメモリを消費してほんの少しだけ速いプログラムよりも、小さなプログラムを好むかもしれない。これは、ウィンドウlook関数と、初期ウィンドウlookの2つの別個の式内に、線のリストを作ることで達成できる。従って、peano peano_nrの結果を共有する代わりに、各参照においてそれを置換える。
changeFigure peano_nr ps=:{io}
# io = setWindowTitle wId ("Peano "+++toString peano_nr) io
# io = setWindowLook wId False (False,\_ _ -> seq (peano peano_nr)) io
= {ps & io = appWindowPicture wId redraw io}
where
redraw = seq [ unfill pictDomain : peano peano_nr ]
これにより、実行時間が余り増大することなく、この約1オーダーの大きさのプログラムのメモリ必要量を減じる。線のリストは、各更新毎に再計算されるが、線は描かれるとすぐにゴミになる。これは、線を保存するのに必要なメモリが描画後すぐに再利用できるということを示している。
最後に述べることとして、changeFigure関数が小さな冗長性を持つことに注目するかもしれない。ウィンドウの外見を設定後、全く同一のlook関数がウィンドウ内に描かれる。True論理値引数をsetWindowLookに渡し、appWindowPictureの適用をスキップすることで、これを組み合わせることができる。
changeFigure peano_nr ps=:{io}
# io = setWindowTitle wId ("Peano "+++toString peano_nr) io
# io = setWindowLook wId True
(True, \_ {newFrame}-> strictSeq [unfill newFrame:peano peano_nr]) io
= {pst & io = io}
リストを回避する
上の議論から、描画関数の長いリストは潜在的な問題であることは、明らかである。オブジェクトIOライブラリのversion 1.2から、それは、描画関数のリスト構築には最早必要ではない。描画は、型*Picture -> *Pictureの関数によって行われる。strictSeqによって評価されなければならない描画関数のリストを構築する代わりに、その描画関数をすぐにピクチャに適用することもできる。
第2の完全に別の変更として、現在の方向を使用したペアノ曲線を描く。この方向を表現する為に、以下のものを定義する。
:: Dir = North | East | South | West
turnRight :: !Dir -> Dir
turnRight d
= case d of
North -> East
East -> South
South -> West
West -> North
turnLeft :: !Dir -> Dir
turnLeft d
= case d of
North -> West
East -> North
South -> East
West -> South
現在の方向を使用すると、ペアノ曲線には基本的に3つの描画可能要素があることが分かる。つまり、直線、C字型曲線及び、D字型曲線と呼ばれるミラーイメージである。曲線の方向は、慣行によって、ペンの動き全体の方向である。これは、図5.12のペアノ1の方向がSouthであるということを示している。その図では、最下レベルのD字型曲線が、左へのターン、直線、右へのターン、直線、右へのターンと、直線からなっていることを理解できる。
曲線内の直線を、他の曲線及び接続線に再帰的に置き換えることは、高階のペアノ曲線を作り出す。例えば、レベルiのD曲線は、左ターン、C曲線(i-1)、直線、右ターン、D曲線(i-1)、直線、D曲線(i-1)、右ターン、直線と、C曲線(i-1)によって置換えられる。
これは、以下のように直接的に実装される。
peano :: !Int !*Picture -> *Picture
peano n pic
# pic = setPenPos {x=len/2, y=len/2} pic
= curveD South n pic
where
curveD :: !Dir !Int !*Picture -> *Picture
curveD d 0 pic = pic
curveD d i pic
# d = turnLeft d
i = i-1
pic = curveC d i pic
pic = line d pic
d = turnRight d
pic = curveD d i pic
pic = line d pic
pic = curveD d i pic
d = turnRight d
pic = line d pic
pic = curveC d i pic
= pic
curveC :: !Dir !Int !*Picture -> *Picture
curveC d 0 pic = pic
curveC d i pic
# d = turnRight d
i = i-1
pic = curveD d i pic
pic = line d pic
d = turnLeft d
pic = curveC d i pic
pic = line d pic
pic = curveC d i pic
d = turnLeft d
pic = line d pic
pic = curveD d i pic
= pic
line :: !Dir -> !*Picture -> *Picture
line d
= case d of
North -> draw {vx = 0 , vy = ~len}
East -> draw {vx = len, vy = 0 }
South -> draw {vx = 0 , vy = len}
West -> draw {vx = ~len, vy = 0 }
len = windowSize / (2^n)
印刷
IOライブラリのモジュールStdPrintは、描画物印刷のプリミティブを提供する。ピクチャを印刷することは、ウィンドウ内の描画に非常に似ている。プリンタの解像力は、通常、画面の解像力より遥かに高いので、少し注意しなければならない。画面上と同一のピクセルを紙片上に描画する場合、非常に小さなピクチャを得る。ライブラリ関数は、紙上のピクセルを拡大するオプションを提供し、紙上に印刷されるピクチャが画面上に描画されるピクチャに類似するようにする。もしプリンタの最高解像力を使用したいならば、プリンタの全てのピクセルを使用する印刷関数を提供しなければならない。
ウィンドウ内の描画とプリンタ上のそれのもう1つの違いは、一連のページを作り出したいかもしれないということである。画面上では、単一のウィンドウの内容を決めるlook関数が存在するだけである。関数printは、描画関数のリストを受け取る。これらの関数の各々は、別々のページで描画する。
モジュールStdPrintTextは、プリンタに高品質テキストを描画するプリミティブを持つ。もし望むなら、ページにヘッダとフッタを印刷することができる。定義モジュールは、説明を含んでいる。
ウィンドウの内容を印刷する最も簡単な方法は、ウィンドウ更新に使用されるlook関数を、描画関数として使用することである。今の例では、look関数は、正しいペアノ曲線描画のために動的に変更される。ペアノ曲線の現在のオーダーは、ウィンドウlook関数内でのみ分かる。この曲線を印刷する為には、プロセス状態にこの情報を保存するか又は、印刷用の現在のlook関数を使用しなければならない。プロセス状態とlook関数内のどの情報が衝突するのかについてのあり得る問題を防ぐ為に、ウィンドウlook関数を使用して、プリンタに描画する。
ペアノプログラムは、印刷を可能にする為に、少し拡張されなければならない。まず、ファイルメニューに、印刷を作動させるメニュー項目を追加する。メニュー定義は以下のものになる。
file = Menu "&File"
( MenuItem "&Print"
[ MenuShortKey 'P'
, MenuFunction (noLS printImage)
]
:+: MenuItem "&Quit"
[ MenuShortkey 'Q'
, MenuFunction (noLS closeProcess)
]
) []
IOライブラリの関数printUpdateFunctionは、印刷を初期化するのに使用される。
printUpdateFunction
:: !Bool (UpdateState -> *Picture -> *Picture) [Rectangle] !PrintSetup !*env
-> (!PrintSetup,!*env) | PrintEnvironments env
この関数の第1引数は、ユーザーに印刷オプションを選択させるダイアログをポップアップするかを決める論理値である。もしダイアログが表示されないならば、印刷は、(システム依存の)デフォルトな方法で発生するだろう。その次の引数は、実際のlook関数である。矩形のリストは、その領域がlook関数によって印刷されるということを決める。プリンタ設定は、プリンタが使用されることを決める抽象データ型である。この関数は常に、画面の解像力をエミュレートする。
ライブラリ関数getWindowLookとdefaultPrintSetupは、現在のlook関数と印刷に必要な現在のプリンタ設定を決める。
getWindowLook :: !Id !(IOSt .l) -> (!Maybe (Book,Look),!IOSt .l)
defaultPrintSetup :: !*env -> (!PrintSetup,!*env) | FileEnv env
printUpdateFunctionに供給される矩形が1つのページに合わない場合、その図は、必要な限り多くのページに描画される。従って、正確に1ページを印刷するには、ページの面積が必要である。これらの面積は、以下のものによってページ設定から選択される。
getPageDimensions :: !PrintSetup !Bool -> PageDimensions
この論理値は、画面の面積をエミュレートしたいか(True)又は、実際のプリンタの解像力を使用したいか(False)を決める。
これらのライブラリ関数を使用すると、メニュー項目printによって呼出されるprintImage関数は、かなりシンプルである。まず、現在のIO状態からそのlookを得ようと試みる。そのようなlookが見つかると、プリンタ設定とページ面積を抽出し、印刷の矩形を計算する。この情報は、printUpdateFunctionに供給される。この関数が出力する新しいプリンタ設定は、sndによって放棄される。もしウィンドウからlookを選択することが失敗すると、関数printImageはすぐに終了する。
printImage ps
# (mb_look,ps) = accPIO (getWindowLook wId) ps
= case mb_look of
Just (_,look)
# (setup,ps) = defaultPrintSetup ps
page_dim = getPageDimensions setup True
rectangle = {corner1=zero, corner2={x=page_dim.page.w-1,y=page_dim.page.h-1}}
-> snd (printUpdateFunction True (look Able) [rectangle] setup ps)
-> ps
5.5.3 テキストを表示するウィンドウ
ファイルを読込む関数を取り、そして、マウスで線を選択(ハイライト)したり、キーボード矢印を使用してスクロールしたりするオプションを拡張したウィンドウに、ファイルの内容を表示するプログラムを作ろう。これは、キーボード及びマウスユーザーインターフェースを持つウィンドウアプリケーションのプログラム方法を例示するシンプルなプログラムをもたらす。
ここでは、オブジェクトI/Oプログラムの典型的なスタートアップコードになじまなければならない。メニューシステムは簡単である。アプリケーションのパブリックな状態は、ファイルの行に対してフィールド - lines -、ある行が選択されるかを指示するフィールド - select -、選択行の行番号を与えるフィールド - selectedline - 、ウィンドウを特定するフィールド - windowid - 、及び、適切なフォントでテキストを描き、フォントメトリックスにアクセスする情報を持つフィールド - textFont - を保持するレコードである。
module displayfileinwindow
import StdEnv, StdIO, commondef
:: ProgState = { lines :: [String]
, select :: Bool
, selectedline :: Int
, windowid :: Id
, textFont :: InfoFont
}
Start :: *World -> *World
Start world
# (fontinfo,world) = accScreenPicture getInfoFont world
# (windowid,world) = openId world
# initstate = { lines = []
, select = False
, selectedline = abort "No line selected"
, windowid = windowid
, textFont = fontinfo
}
= startIO SDI initstate openmenu [] world
where
openmenu = snd o openMenu undef menu
menu = Menu "&File"
( MenuItem "&Open"
[ MenuShortKey 'O'
, MenuFunction (noLS (FileReadDialog Show))
]
:+: MenuSeparator []
:+: MenuItem "&Quit" [MenuShortKey 'Q', MenuFunction (noLS Quit)]
) []
Quit pst = closeProcess pst
関数Showは、ファイルを開き、その内容を読込み、DisplayInWindowを呼出してウィンドウに結果を表示する。
Show :: String (PSt ProgState) -> PSt ProgState
Show name pst=:{ls}
# (readok,file,pst) = sfopen name FReadText pst
| not readok = abort ("Could not open input file '" +++ name +++ "'")
# lines = LineListRead file
| isEmpty lines = pst
| otherwise = DisplayInWindow {pst & ls = {ls & lines = lines}}
我々は、一度にただ1つのウィンドウを表示することを意図しているので、まず、DisplayInWindowは、前のウィンドウを閉じなければならない。これは、実行時エラーの原因とはならないことに注意して欲しい。というのも、closeWindowは、閉じるべきウィンドウが無いならば単にスキップするからである。SDIアプリケーションのウィンドウが既にある場合に、新しいウィンドウを開こうとすると、それもスキップされる。
DisplayInWindow :: (PSt ProgState) -> PSt ProgState
DisplayInWindow pst=:{ls=state=:{textFont,windowid,lines}}
# pst = closeWindow windowid pst
= snd (openWindow undef windowdef pst)
where
windowdef
= Window "Read Result"
NilLS
[ WindowHScroll (stdScrollFunction Horizontal textFont.width)
, WindowVScroll (stdScrollFunction Vertical textFont.height)
, WindowViewDomain { corner1={x= ~whiteMargin,y=0}
, corner2={x= maxLineWidth,y=length lines*textFont.height}
}
, WindowViewSize {w=640,h=480}
, WindowLook False (look state)
, WindowKeyboard filterKey Able (noLS1 handleKeys)
, WindowMouse filterMouse Able (noLS1 handleMouse)
, WindowId windowid
]
whiteMargin = 5
maxLineWidth = 1024
スクロールの単位と領域のサイズは、アプリケーションのデフォルトフォントから取られるフォントサイズを使用して定義される。これらの値は、関数getInfoFontにより計算され、型InfoFontのレコードに保存される。フォントのメトリックスは、描画環境の解像力に依存しているので、関数getInfoFontは、実際には、Picture環境へのアクセス関数である。この場合、我々は画面の解像力に関心があるので、アプリケーションの起動時にはaccScreenPictureを使用して、一時的な画面ピクチャ環境を生成する訳である。accScreenPictureの型は多重定義化されていることに注目して欲しい。つまりこれは、Worldと(IOSt .l)環境に適用できるのである。
:: InfoFont = { font :: Font
, width :: Int
, height :: Int
, up :: Int
}
getInfoFont :: *Picture -> (InfoFont,*Picture)
getInfoFont env
# (font,env) = openDefaultFont env
# (metrics,env) = getFontMetrics font env
= ( { font=font
, width = metrics.fMaxWidth
, height = fontLineHeight metrics
, up = metrics.fAscent+metrics.fLeading
}
, env
)
前に説明したように、ウィンドウのlook関数属性は、そのウィンドウ(の一部)を再描画しなければならない場合、オブジェクトI/Oシステムによって、自動的に呼出される。これは、再描画に必要な領域のリストと、ウィンドウの*Picture環境の現在の値に適用される。
この場合、look関数は、パブリックな状態レコードによってもパラメータ化され、必要な情報への容易なアクセスが与えられる。プログラムをシンプルに保つ為に、線の一部が再描画領域の外側にある場合にさえ、その完全な線が描かれる(これは、非常に小さな不効率性の他には可視的な効果を持たない)。
look :: ProgState SelectState UpdateState -> (*Picture -> *Picture)
look state =:{select,selectedline,textFont,lines} _ updstate =: {updArea}
= strictSeq (map update updArea)
where
update domain=:{corner1=c1=:{y=top},corner2=c2=:{y=bot}}
= drawlines (tolinenumber textFont top) (tolinenumber textFont (dec bot)) lines o
unfill {corner1={c1 & x= ~whiteMargin},corner2={c2 & x=maxLineWidth}}
drawlines first last textlines pict
# pict = strictSeq [ drawAt {x=0,y=y} line
\\ line <- textlines%(first,last)
& y <- [init_y, init_y + textFont.height..]
] pict
| select && (selectedline >= first || selectedline <= last)
= hiliteline textFont selectedline pict
| otherwise
= pict
where
init_y = towindowcoordinate textFont first + textFont.up
ライブラリの描画関数は、勿論、ウィンドウ領域のウィンドウ座標を使用するが、各プログラムは通常は、状態(の一部)内に自己の座標を持っている。従って、プログラムは通常は、様々な座標の集合間の変換関数を保持する。
この場合には、プログラムはウィンドウ座標を線番号に変換しなければならないし、逆もそうである。これは明らかに、そのフォントメトリックスに依存している。
tolinenumber font windowcoordinate = windowcoordinate / font.height
towindowcoordinate font linenumber = linenumber * font.height // 線のトップ
これらの変換を使用すると、線をハイライト化する関数を書くことは単純である。ハイライトは、前に見たDrawablesクラスと同一のシグニチャを持つ型構成子クラスHilitesの多重定義関数hiliteとhiliteAtによって行われる。ハイライト化できるインスタンスは、箱と矩形のみである。
hiliteline font linenr = hilite (towindowrectangle font linenr)
towindowrectangle font linenumber
= { corner1 = {x = ~whiteMargin, y = winco }
, corner2 = {x = maxLineWidth, y = winco + font.height}
}
where
winco = towindowcoordinate font linenumber
キーボードハンドラ
ウィンドウキーボード属性関数は、その親ウィンドウが有効にされ、アクティブであり、キーボード入力がそのキーボードイベントフィルタによって受理される場合に、オブジェクトI/Oシステムによって呼出される。キーボード状態値は、キーボード情報(キーは押されたか?どのキーが?Shift、alt/option、command又はcontrol downのようなメタキー又は修正子が押されたか?)を記録する。キーボード関数はコールバック関数なので、もちろん、そのプロセス状態にも適用される。その結果は、修正されたプロセス状態である。キーボード情報に関連付けられた述語は、関数が関連するケースをフィルターする。この関数は、関数実装を簡単にするのに使用できる。もし全てのキーボードイベントの取得に興味があるならば、式(const True)はトリックを行う。我々の小さなプログラムでは、フィルターは、関数filterKeyによって定義される。この場合には、プログラムは押されている特別なキー(カーソルキー及び、ページアップ/ダウンキー)にのみ興味を持つ。これは以下のようになされる。
filterKey (SpecialKey _ keyState _) = keyState <> KeyUp
filterKey _ = False
キーボード関数handleKeysは、今度はウィンドウlook関数を内部で適用するmoveWindowViewFrameを呼出す。スクロールベクタの計算は簡単である。ページダウンとページアップの場合にベクタを計算するには、現在のウィンドウビューフレームサイズが必要とされる。この情報は、(StdWindowの)関数getWindowViewFrameによって取得される。(StdIOCommonで定義される)関数rectangleSizeは、RectangleのSizeを返す。Sizeは、各々幅及び高さを表す整数フィールドw及びhを持つレコード型であることを思い出して欲しい。
handleKeys :: KeyboardState (PSt ProgState) -> PSt ProgState
handleKeys (SpecialKey kcode _ _) pst=:{ls={textFont=font,windowid},io}
# (frame,io) = getWindowViewFrame windowid io
= {pst & io = moveWindowViewFrame windowid (v (rectangleSize frame) .h) io}
where
v pagesize
| kcode==leftKey = {zero & vx= ~font.width}
| kcode==rightKey = {zero & vx= font.width}
| kcode==upKey = {zero & vy= ~font.height}
| kcode==downKey = {zero & vy= font.height}
| kcode==pgUpKey = {zero & vy= ~pagesize}
| kcode==pgDownKey = {zero & vy= pagesize}
= zero
マウスハンドラ
ウィンドウマウス属性関数は、その親ウィンドウが有効にされ、アクティブであり、マウス入力がそのマウスイベントフィルタによって受理される場合に、オブジェクトI/Oシステムによって呼出される。マウス状態値は、マウス情報(位置、クリックがないのか/1つなのか/2つなのか/3つなのか/長いのか、修正子キーは押されているのか)を記録する。キーボード関数と同じ方法で、マウス関数は、そのプロセス状態を修正する。マウス情報に関連付けられた述語は、関数に関連のあるケースをフィルターする。この関数は、関数実装を単純にするのに使用できる。もし全てのマウスイベントを取得することに関心があるなら、式(const True)は、トリックを行う。このプログラムでは、プログラムはダブルダウンマウスイベントに関心があるだけである。これは以下のようになされる。
filterMouse (MouseDown _ _ 2) = True
filterMouse _ = False
マウス関数handleMouseは、選択された線を変更し、(再びハイライトすることで)古いもののハイライトを消し、その新しい選択をハイライトする。プログラムのパブリックな状態は、マウス関数によって変更されるので、ウィンドウlook関数'insync'を取得しなければならない。というのも、それはパブリックな状態でパラメータ化されるからである。
handleMouse :: MouseState (PSt ProgState) -> PSt ProgState
handleMouse (MouseDown {y} _ _) pst=:{ls=state=:{textFont,select,selectedline,windowid},io}
# io = appWindowPicture windowid (changeselection selectedline selection) io
= {pst & ls = newstate
, io = setWindowLook windowid False (False, look newstate) io
}
where
selection = tolinenumber textFont y
newstate = {state & select = True,selectedline = selection}
changeselection old new
| select = hiliteline textFont new o hiliteline textFont old
| otherwise = hiliteline textFont new
図5.13 自身のソースを読込んだ場合のファイル表示プログラムの図

5.6 タイマ
ダイアログ又はウィンドウシステムを定義することによってユーザーのイベントに反応することとは別に、指定時間のインターバルが過ぎるとシステムが生成するタイマイベントに対して呼出されるコールバック関数を持つ、タイマデバイスを定義することもできる。
これは、一定規則に基づく情報を表示したり、例えばシューティングゲームで情報を変更したりするのに使用することができる。タイマを使用するもう1つの方法は、一定規則で編集ファイルを保存するエディタの自動保存機能のようなある種のバックグラウンド動作を生成することである。
タイマを加えることは、ダイアログやメニューを加えることに非常に似ている。つまり、それらはstartIO関数の初期化関数において生成できる(しかし勿論、異なった時期にである)。タイマはIdで特定できる。それらは、タイマインターバルと、そのタイマインターバルが経過するといつでも実行されなければならないコールバック関数によって特徴付けられる。タイマは、有効にも無効にもできるが、そうする為には、Idを知らなければならない。以下では、5分毎に表示されたファイルをコピーして保存する単一のタイマに関する定義を示す(timerIdは型Idの値であるものとみなすことに注意)。
opentimer = snd o openTimer undef timer
time
= Timer TimerInterval // (チック単位)タイマインターバル
NilLS // タイマは要素を持っていない
[ TimerId timerId // タイマのId
, TimerSelectState Unable // タイマは最初は動作していない
, TimerFunction (noLS1 timerfunction) // 作動する場合の動作
]
where
TimerInterval = 300 * ticksPerSecond // 300秒 = 5分
timerfunction nrofintervalspassed // セクション5.4の通知関数参照
= inform ["Hello"]
このようなタイマは、自動保存関数に使用できるだろう。選択されると、それは、メニュー項目タイトルを留めておき、もし自動保存が必要ならば、タイマを有効に変え(及び、逆にもす)る。これもメニュー項目の局所状態の良い使用例である。これは、自動保存オプションがオンであるかを述べる論理値をキープしている。初期はオフであるので、初期論理値は、Falseである。メニュー項目を選択することは、論理値を否定にし、それに従って、メニュー項目のタイトル及びタイマのSelectStateを変更する。
...
{ newLS = False // メニューアイテムの局所状態、初期値
, newDef = MenuItem "Enable AutoSave"
[ MenuId autoSaveId
, MenuShortKey 'S'
, MenuFunction AutoSave
]
}
...
AutoSave :: (Bool,PSt .l) -> (Bool,PSt .l)
AutoSave (autosave,pst)
= ( not autosave
, appListPIO
[ toggle timerId
, setMenuElementTitles [(autoSaveId,title)]]
] pst
)
where
(toggle,title) = if autosave (disableTimer,"Enable AutoSave" )
(enableTimer, "Disable AutoSave")
演習5.6に必要とされるプログラムの変更の多くも、この自動保存関数が必要であることに注意。
5.7 線描画プログラム
上に紹介した全てのピースがどのようにして一緒になって調和するかを示すためには、完全なウィンドウベースのプログラムを示そう。そのプログラムはシンプルな線描画ツールである。これは完全な描画プログラムとして意図されてはいないが、そのようなプログラムの構造を例解する。プログラムのサイズを限定するために、基本的な可能性を制限する。結果として、描画プログラムの望ましいオプションが多く欠落している。これらの特徴を加えるには新しい技術を必要としない。
操作されるGUI要素の側では、このプログラムはかなり完全である。勿論それは、描画物をユーザーに表示し、マウスを使った操作によって、線を生成及び変更することを認めるウィンドウを持つ。ユーザーは、キーボードを使用して、イメージをスクロールするか、加えられた最後の線を削除することができる。そのイメージは、ファイルに保存でき、ファイルから読込可能である。ユーザーにそのピクチャの保存を思い出させるタイマが存在する。ダイアログは、ヘルプ関数や標準のaboutダイアログに使用できる。
このプログラムをlinedrawと呼ぶ。これは、標準データ構造と計算(StdEnv)並びに、オブジェクトI/Oシステム(StdIO)を操作するのに使用される型定義と関数を持つ、必要とされるモジュールをインポートすることで始まる。適当な時ならばいつでも、.dclファイルを読込むことを勧める。これらのファイルは、適用することが認められる構成物はどれかを決め、これらの構成物の使用法と意味論についての有用なコメントを持っている。モジュールnoticeは、セクション5.4.5で論じた通知の実装を保持する。
module linedraw
import StdIO,StdEnv,StdId,notice
このプログラムはウィンドウの内側の描画を処理するので、ウィンドウ座標を使用する。その原点は、左上端であり、点は1対の整数で指示される。
プログラム状態
第1ステップとして、プログラムの重要なデータ型を定義する。プログラムのパブリックな状態のデータ構造として、拡張可能なレコードを使用する。これによって、容易に拡張を加えることが認められるからである。この方法では、プログラムを段階的な方法で開発できる。最初のバージョンでは、レコード内に、2つのデータ片を保存する。つまり、(1)このプログラムは純粋な線描画ツールであるつもりなので、- レコードフィールドlines内の - 線のリストでも、ユーザーが描いた線の軌跡を保持するのに十分であり、(2)- レコードフィールドfname内で - 使用される最後の入出力ファイルの名前、これは、我々が再度イメージを保存する場合のデフォルトである。これらのフィールドの初期値は、[](最初に描かれる線)と""(ファイルが選択されていない)である。
:: ProgState = { lines :: [Line] // 描画
, fname :: String // 描画を保存するファイル名
}
InitProgState = { lines = []
, fname = ""
}
オブジェクトI/Oライブラリで線を描くことは、点やベクタを経由して行われる。いずれにせよ線を保存するつもりなので、線を描くDrawables型構成子クラスの新しいインスタンスを導入するいい機会である。
:: Line = { end1 :: !Point2
, end2 :: !Point2
} // 線は2つの端点により定義される
// 訳者注:Point2は定義済み(5.5参照)
instance zero Line
where
zero = { end1 = zero, end2 = zero }
instance Drawables Line
where
draw {end1,end2} p = drawLine end1 end2 p
drawAt _ {end1,end2} p = drawLine end1 end2 p
undraw {end1,end2} p = drawInBackColour (drawLine end1 end2) p
undrawAt _ {end1,end2} p = drawInBackColour (drawLine end1 end2) p
drawInBackColour :: (IdFun *Picture) *Picture -> *Picture
drawInBackColour f p
# (pen, p) = getPenColour p
# (back,p) = getPenBack p
# p = setPenColour back p
# p = f p
= setPenColour pen p
このパブリック状態に加えて、線描画の際にマウスが使用する局所状態を、このウィンドウに加えることを決める。ここでは、Maybeデータ型が簡便である(これはモジュールStdMaybeで定義されている)。マウスが実際に跡を付けている時にのみ、線はウィンドウ状態に保存されるのであって、そうでなければそれは単にNothingである。また、将来の拡張を認めるために、ウィンドウの局所状態にはレコード型を使用する。
:: WindowState = { trackline :: Maybe Line
}
InitWindowState = { trackline = Nothing
}
大域構造
このプログラムをトップダウンに記述する。これは、我々がStart規則から始めるということを示す。プログラムの最初の部分は今まででかなり標準的であるはずである。従って、関連する全てのコードを示す。前バージョンとの唯一の違いは、初期アクション - initialIO - は、描画ウィンドウ及びタイマの各々のIdでパラメータ化されるということである。更なる全てのデバイス定義は、初期化関数initialIOに局所的であり、従って、これらのIdを参照できる。初期化関数は、タイマ、ウィンドウ及び2つのメニューを開く。
Start :: *World -> *World
Start world
# (wId,world) = openId world
# (tId,world) = openId world
= startIO SDI InitProgState (initialIO (wId,tId)) [] world
where
initialIO (wId,tId)
= openeditmenu
o openfilemenu
o openwindow
o opentimer
openfilemenu = snd o openMenu undef file
file = Menu "&File"
( MenuItem "&About" [ MenuShortKey 'I', MenuFunction openabout ]
:+: MenuSeparator []
:+: MenuItem "&Open" [ MenuShortKey 'O', MenuFunction (noLS Open) ]
:+: MenuItem "&Save" [ MenuShortKey 'S', MenuFunction (noLS Save) ]
:+: MenuSeparator []
:+: MenuItem "&Quit" [ MenuShortKey 'Q', MenuFunction (noLS Quit) ]
) []
openeditmenu = snd o openMenu undef edit
edit = Menu "&Edit"
( MenuItem "&Remove Line" [ MenuShortKey 'R', MenuFunction (noLS Remove) ]
:+: MenuSeparator []
:+: MenuItem "Help" [ MenuShortKey 'H', MenuFunction (noLS Help) ]
) []
openwindow = snd o openWindow InitWindowState window
window = Window "Picture" // ウィンドウタイトル
NilLS // コントロールを持たない
[ WindowId wId // ウィンドウId
, WindowHScroll (stdScrollFunction Horizontal 10) // 水平スクロールバー
, WindowVScroll (stdScrollFunction Vertical 10) // 垂直スクロールバー
, WindowViewDomain PictDomain // ウィンドウのビュードメイン
, WindowViewSize InitWindowSize // ウィンドウの初期サイズ
, WindowLook False (\_ _ -> look []) // ウィンドウの外見
, WindowMouse filterMouse Able HandleMouse // マウス処理
, WindowKeyboard filterKey Able (noLS1 HandleKey) // キーボード処理
, WindowClose (noLS Quit) // クローズはプログラムを終了する
]
opentimer = snd o openTimer undef timer
timeer = Timer time NilLS
[ TimerId tId
, TimerFunction (noLS1 RemindSave)
]
このようなプログラム開発中は、よりシンプルなユーザーインターフェースから始めることができる。実際、この例の開発中にはタイマなしで始めた。またヘルプ関数と線を削除する可能性は最初のプロトタイプでは存在していなかった。
段階的方法でプログラムを構築すると便利である。このプログラムの非常にシンプルなバージョンから始め、一つ一つ拡張を加える。プログラムは、各々次の追加の前にコンパイルされテストされる。全てのコールバック関数はオプションなので、関数属性から全く始めないことも、無演算を使用することも容易である(その為に、StdFunc関数idは有用であることが証明されている)。また、ウィンドウlook属性は、プログラムの最初の近似では存在しないか無演算ということでもよい(これは一般に、プログラムが正当かどうかを「見る」ことを困難にするだろうが)。ウィンドウのマウスハンドラとキーボードハンドラは最初は省略できるし、又は、再び無演算を使用することもできる。
また、対話型プログラムで定義する最初のことは、その停止性(termination)である。メニューコマンドQuitによってこれを行う。これによって、下降法でプログラムのテスト版を省略することができる。今までで分かっているはずだが、あらゆる対話型プログラムを終了する唯一の関数は、(モジュールStdProcessの)closeProcessである。Quitの実装は、それ故に、プロセスを閉じるのが単純である(startIOは、1つの対話型プロセスを生成し、それの対話型プロセスの全てが終了した場合にのみ終了することを思い出して欲しい)。
Quit :: (PSt .l) -> PSt .l
Quit pst = closeProcess pst
マウス処理
次に実装することは、このプログラムの基本的な部分たるマウス処理である。これは、一般的によい戦略である。つまり、できるだけ早くプログラムのハードできつい部分を行う。プログラムを完成する単純で細かな部分は後で付け加えることができる。その困難な部分は構築中のプログラムの成功のほとんどを決めるようである。依然として基本的変更が危険なプログラムに関する単純作業に時間を費やすことは許されない。
マウスについてしなければならない最初のことは、線を描くことである。線は、マウスボタンを押している点から始まり、マウスボタンが放される所で終わる。マウスがドラッグされている間は、構築中の線はラバーバンドのように描かれる。この抽象的な説明(仕様)は既に、我々が関心を持っているマウスイベントは何かを伝えている。つまり、任意数のマウスドラッグが続いたマウスダウンと、マウスアップイベント。これは、以下のmouseFilter関数によって表現される。
filterMouse :: MouseState -> Bool
filterMouse (MouseDown _ _ _) = True
filterMouse (MouseDrag _ _ ) = True
filterMouse (MouseUp _ _ ) = True
filterMouse _ = False
前に論じたように、ウィンドウは、局所状態、型WindowStateのレコードを持つ。マウスコールバック関数は、自身が適切に動作するのに必要な情報を保存している。マウスコールバック関数は、フィルタされたMouseState値でパラメータ化される。これらは常に、マウスの現在の位置を保持する。忘れてはならない唯一のことは、線の開始点と、描かれた線の前のバージョンを消す為の前の端点である。抽象仕様が提案するように、このラバーバンドを描くことは、3つの段階からなり、各段階は、HandleMouseの1つの関数代替部によって適切に定義される。その代替部は、MouseState代替構成子MouseDown、MouseDrag及びMouseUpのパターン照合である。それらを順次論じる。
マウスボタンが押されると、HandleMouseは、ウィンドウ状態の現在のマウス位置を保存する。(timerOffを使って)タイマを無効化する。というのも、線の描画に干渉したくないからである。
HandleMouse (MouseDown pos _ _) (window,pst)
= ({window & trackline=Just {end1=pos,end2=pos}},timerOff pst)
ユーザーがマウスをドラッグしている間に、HandleMouseはまず、前に跡付けた線を消し、新しい跡付けをする線を描く。その新しい跡付け線は、ウィンドウ状態に保存される。この方法での処理は、ラバーバンドの効果をもたらす。描画関数appXorPictureは、既存のピクチャにダメージを与えることを防ぐのに使用される。XorModeでオブジェクトを2回描くことで、オリジナルのピクチャを復旧する。揺らめきを防ぐ為に、マウスが同一位置にある場合(第1ガードにテストされる場合)には再描画は抑制される。
HandleMouse (MouseDrag pos _) (window=:{trackline=Just track},pst)
| pos == track.end2
= (window,pst)
| otherwise
# newtrack = {track & end2=pos}
= ( { window & trackline=Just newtrack }
, appPIO (appWindowPicture wId (appXorPicture (draw track o draw newtrack))) pst
)
マウスボタンが放されると、線は完成し、跡付けは終了する。従って、ウィンドウ状態はNothingにリセットされ、新しい線はパブリックプログラム状態に加えられる。ピクチャは変化したので、(timerOnを使って)タイマはまたオンに切りかえられる。ウィンドウのlook関数も更新されなければならない。線は既に可視的なので、setWindowLookのFalse論理値引数により指示されるウィンドウをリフレッシュする必要はない。
HandleMouse (MouseUp pos _) (window=:{trackline=Just track},pst=:{ls=progstate=:{lines}})
# pst = {pst & ls=newprogstate}
# pst = timerOn pst
# pst = appPIO (setWindowLook wId False (False,\_ _ ->look newlines)) pst
= ({window & trackline=Nothing},pst)
where
newline = {track & end2=pos}
newlines = [newline:lines]
newprogstate = {progstate & lines=newlines}
これらの関数により、プログラムをコンパイルして、いくつかの線を描くことができる。すぐに描画を変更できることが望ましいと分かるだろう。ピクチャを変更する非常にシンプルな方法は、描いた最後の線を削除することである。この為に、コールバック関数Removeを持つメニューコマンドRemove Lineを導入する。もし削除される線に重複している線があるならば、この線を削除するだけでは十分ではない。この世界は重複している線に穴を作る。我々は単純にピクチャ全体を消し、再び全ての残りの線を描く。今回は、可視的なウィンドウ領域全体のリフレッシュを引き起こすsetWindowLookの最初のTrue論理値引数をTrueに設定することで、これを達成する。更に幾ばくかのプログラミングの努力により、描画量を減じることができるが、この努力を投じる理由は現在の所はない。もし線のリストが空ならば、削除する線はない。このエラーを示すのにコンピュータビープを鳴らす。
Remove :: (PSt ProgState) -> PSt ProgState
Remove pst=:{ls={lines=[]}}
= appPIO beep pst
Remove pst=:{ls=state=:{lines=[_:rest]},io}
= { pst & ls = {state & lines=rest}
, io=setWindowLook wId True (False,\_ _ -> look rest) io
}
look :: [Line] *Picture -> *Picture
look ls picture
= foldr draw (unfill PictDomain picture) ls
ピクチャを変更するもう1つの方法は、既存の線を編集することである。ユーザーが線の端の1つに非常に近くで、シフトキーを押しながらマウスボタンを押すと、その線は変更できる。まさに線の端ではなく、線の端の非常に近くで使用する。というのも、ユーザーが、線のちょうど最後にマウスを位置設定することは難しいからである。
関数HandleMouseを変更する。まず、シフトキーが押されているかを検査する。もしそうならば、線の端にマウスが触れていることを見付けようとするだろう。もしそのような線が発見されたら、その状態からその線を取り除き、初期バージョンとして、削除した線からその線を描き始める。もし線が触れていないならば、プログラムはこのマウスイベントを無視する。もしシフトキーが押されていないならば、関数HandleMouseの前バージョンの場合のように進める。CLEANでは、関数代替部はテキストの順序で評価されるので、MouseDownにパターン照合するHandleMouseのその他の代替部の前に新しい代替部を加えることで十分である。
HandleMouse (MouseDown pos {shiftDown} nrDown) (window,pst=:{ls=state=:{lines}})
| shiftDown
= case touch pos lines of
Just (track,ls) -> ( { window & trackline = Just track }
, timerOff { pst & ls = {state & lines=ls} }
)
Nothing -> HandleMouse (MouseDown pos NoModifiers nrDown) (window,pst)
関数touchは、ある点が与えられた線の一端に非常に近いかどうかを決める。この関数は、論理値を出力する代わりに、型Maybeを使用する。成功の場合には、触れた線と他の全ての線のリストを返し、そうでなければ、Nothingである。
touch :: Point2 [Line] -> Maybe (Line,[Line])
touch p [] = Nothing
touch p [line=:{end1=s,end2=e}:rest]
| closeTo p s = Just ({end1=e,end2=s},rest)
| closeTo p e = Just (line,rest)
| otherwise = case touch p rest of
Just (l,rest`) -> Just (l,[line:rest`])
Nothing -> Nothing
where closeTo {x=a,y=b} {x,y} = (a-x)^2 + (b-y)^2 <= 10
ファイルIO
次に、ファイルに描画物を保存し、それをまた読込めるようにしたい。各線は、その端点によって表される。これらの点の各々は2つの整数からなる。従って、線は、データファイルに、4つの整数の並びとして保存される。StdFileSelectの標準的なプラットフォーム依存の出力ファイル選択ダイアログ(output file selector dialogue)を使用して、ユーザーに、出力ファイルの名前を決めさせる。
Save :: (PSt ProgState) -> PSt ProgState
Save pst=:{ls=state=:{fname,lines}}
# (maybe_fn,pst) = selectOutputFile "Save as" fname pst
| isNothing maybe_fn
= pst
# fn = fromJust maybe_fn
# (ok,file,pst) = fopen fn FWriteData pst
| not ok
= inform ["Cannot open file"] pst
# file = seq [ fwritei i
\\ {end1,end2} <- lines
, i <- [end1.x,end1.y,end2.x,end2.y]
] file
# (ok,pst) = fclose file pst
| not ok
= inform ["Cannot close file"] pst
| otherwise
= {pst & ls={state & fname=fn}}
ファイルの開閉について何か間違いがある場合には、セクション5.4.5で導入したinform通知を再利用した。
ファイルを開くことと、ファイルから線を読み込むことは非常に似ている。再び、(またStdFileSelectで定義されている)プラットフォーム依存の入力ファイル選択ダイアログ(input file selector dialogue)を使用して、ユーザーにファイルの選択を親切に尋ねる。再利用を認めるために、一意なファイルとしてその選択ファイルを開く。この方法では、ファイルから描画物を読込み、それを変更し、再び同一ファイルに保存することができる。ファイル内で見つかる4つの整数の各並びは、線として解釈される。ファイル形式については何らの検査もしない。
Open :: (PSt ProgState) -> PSt ProgState
Open pst=:{ls=state}
# (maybe_fn,pst) = selectInputFile pst
| isNothing maybe_fn
= pst
# fn = fromJust maybe_fn
# (ok,file,pst) = fopen fn FReadData pst
| not ok
= inform ["Cannot open file"] pst
# (ints,file) = readInts file
# (ok,pst) = fclose file pst
| not ok
= inform ["Cannot close file"] pst
| otherwise
# lines = toLines ints
# pst = appPIO (setWindowLook wId True (False,\_ _ -> look lines)) pst
= {pst & ls={state & lines=lines,fname=fn}}
where
toLines :: [Int] -> [Line]
toLines [a,b,x,y:r] = [{end1={x=a,y=b},end2={x=x,y=y}}:toLines r]
toLines _ = []
readInts :: *File -> ([Int],*File)
readInts file
# (end,file) = fend file
| end = ([],file)
# (ok,i,file) = freadi file
| not ok = ([],file)
# (is,file) = readInts file
= ([i:is],file)
キーボードハンドラ
次のステップとして、キーボードインターフェースを、描画プログラムのウィンドウに付け加える。矢印キーはウィンドウをスクロールし、バックスペース及び削除キーは、メニューアイテムRemoveと同等である。これらのキーは全て、KeyboardState型のSpecialKey代替構成子に属する。KeyUpイベントを無視することも意図している。これらの考慮は、以下のキーボードフィルターfilterKeyを導き出す。
filterKey :: KeyboardState -> Bool
filterKey (SpecialKey _ kstate _) = kstate<>KeyUp
filterKey _ = False
このキーボードフィルタの定義によって、ウィンドウのキーボードコールバック関数HandleKeyは、特別なキーの処理のみをすればよい。それの第一代替部は、単純にバックスペースと削除キーをチェックする。もしそれがこれらのキーの1つならば、HandleKeyは、Removeのように処理する。HandleKeyの他の代替部は、他の全てのスクロールの特別なキーを処理する。それは、セクション5.5.3で論じたキーボード関数に非常に似ている。再び、ウィンドウの現在の面積を決めるのにgetWindowViewFrameを使用する。その特別なキー如何により、moveWindowViewFrameを使ってウィンドウビューフレームは移動される。
HandleKey (SpecialKey kcode _ _) pst
| isMember kcode [backSpaceKey,deleteKey]
= Remove pst
HandleKey (SpecialKey kcode _ _) pst=:{io}
# (frame,io) = getWindowViewFrame wId io
= {pst & io = moveWindowViewFrame wId (v (rectangleSize frame) .h) io}
where
v pagesize
| kcode==leftKey = {zero & vx= ~10}
| kcode==rightKey = {zero & vx= 10}
| kcode==upKey = {zero & vy= ~10}
| kcode==downKey = {zero & vy= 10}
| kcode==pgUpKey = {zero & vy= ~pagesize}
| kcode==pgDownKey = {zero & vy= pagesize}
| otherwise = zero
タイマ
付け加える最後のGUI要素はタイマである。描画物の最初の変更からの予め定義した時間経過後に、通知がユーザーに示される。この通知によって、ユーザーは、自己の仕事を保存することを思い出す。この通知には2つのボタンがある。ボタン"Save now"は、関数Saveを呼出す。その他のボタンはタイマをリセットする。タイマは、最初にそれを無効にして、それから有効にすることによってリセットできる。これは、関数timerResetにより実装される。
timerReset tId = appListPIO [disableTimer tId,enableTimer tId]
ユーザーに代わって、そのいらいらする要因を低くしておく為に、2つの関数timeOffとtimeOnを加える。それは、ユーザーが線を描く場合に通知がポップアップすることを防止する。disableTimer及びenableTimerを単純に呼び出すことがそのトリックであると想定するかもしれない。このナイーブな実装は動作しない。このような込み入っていることの理由は、無効なタイマに適用されると、enableTimerはタイマの最新のタイムスタンプをリセットするということである。このことのために、単純なアプローチでは、ユーザーが何かを描画する場合にいつでも常にタイマを延期するということになるだろう。必要なのは、ユーザーを悩ますことを認めるかをタイマに伝える追加的な局所状態の部分である。ここでは、プログラム状態ProgStateがレコードであるという事実から利益を得ることができる。これに新しいフィールドnoticeOKを拡張して、それを以下の初期値に与えなければならない。
:: ProgState = { lines :: [Line] // 描画した物
, fname :: String // 描画を保存するファイル名
, noticeOK :: Bool // タイマは干渉すべきでない
}
InitProgState = { lines = []
, fname = ""
, noticeOK = True
}
ユーザーを保護する関数timerOffは、単純にnoticeOKフィールドをFalseに設定する。
timerOff :: (PSt ProgState) -> PSt ProgState
timerOff pst =: {ls=state} = {pst & ls={state & noticeOK=False}}
もしタイマの間隔が過ぎたら、タイマコールバック関数RemindSaveは、noticeOKフラグを検査する。もしそれが干渉することになっていないならば、何もしない。そうでなければ、それは幸せにもユーザーに干渉する。
RemindSave :: NrOfIntervals (PSt ProgState) -> PSt ProgState
RemindSave _ pst=:{ls=state=:{noticeOK}}
| noticeOK = timerReset tId (openNotice notice pst)
| otherwise = pst
where
notice = Notice ["Save now?"] (NoticeButton "Later" id)
[ NoticeButton "Save now" (noLS Save) ]
関数timerOnに対しては、2つのケースがある。つまり、noTimerフラグが安全にFalseに設定できる場合において、それがTrueである間は、タイマは干渉したくなかったか、又は、タイマは干渉したかったが認められていなかったかのどちらかである。後者の場合にはタイマがはっきりしたイニシアチブを取ってフラグをFalseにセットするので、その状況を検知する。この場合には、timerOnは単純に、遅れた動作としてタイマ関数を呼出す。
timerOn :: (PSt ProgState) -> PSt ProgState
timerOn pst=:{ls=state=:{noticeOK}}
| noticeOK = RemindSave undef pst
| otherwise = {pst & ls={state & noticeOK=True}}
最後に、プログラムで使用されるいくつかの定数がある。最初の3つの定数は、描画ウィンドウの属性を決める。値timeは、保存の問い合わせの間隔を決める。
PictDomain :== {zero & corner2={x=1000,y=1000}}
InitWindowSize :== {w=500,h=300}
time :== 5*60*ticksPerSecond // 保存問い合わせの間隔
これで我々の線描画例は完成する。これは、どのようにして上に紹介した全ての部分がまとめられて完全なプログラムを生成できるかを例証する。それは、プログラムに特徴を加えて、それをより良い描画ツールにする気にさせる。例えば、保存問合わせのスイッチを切り替えたり、そのタイム間隔を設定する。線の太さを設定するオプションは、円、矩形等々と同様によいものだろう。これらのものを付け加えるのに新しい技術は不要である。この例のサイズを限定する為に、ユーザーにこれらの拡張の制作を委ねる。第2部第4章は、より洗練された描画ツールを議論する。
5.8 演習
- 文字リストから与えられたファイルの文字リストに変換する、与えられた関数を適用するプログラムを書きなさい。その変換関数を引数として提供できるようなプログラムを構成しなさい。通常の文字を大文字に変換する関数を持ち、かつ、行を集め、それらをソートし再び文字リストに結合する関数を持つプログラムをテストしなさい。
- FileReadDialogとFileWriteDialog関数を、ユーザーがダイアログで指示したように反復してファイルをコピーする完全なコピーファイルプログラムに組み合わせなさい。
- ユーザーがダイアログで指示する通りにファイルを変換するように、演習5.1で作ったプログラムを修正しなさい。
- ウィンドウ内に以下の曲線の1つを生成するプログラムを書きなさい。

- ユーザーが、閲覧したファイルをSelectOutputFileダイアログで保存できるように、ファイル表示プログラムを修正しなさい。関数FileWriteDialog(のバリアントをできる限り)使いなさい。遅く、ではなくすぐに保存されることを保証する為に、!マークをProgStateの型定義のFilesに前置きすることで、ProgStateのFilesコンポーネントを正格にすることができる。状態レコードフィールドとしてテキストを追加しなさい。ファイル名とファイルそれ自体をこの状態に付け加えることも有用であることが明らかになるだろう。ユーザーの表示ファイル上書きを認めるには、プログラムは、sfopenではなく表示用のfopenを使用して変更されなければならない。というのも、sfopenで開いたファイルは更新も閉じることもできないからである。
- ウィンドウ内にファイルの変換情報の結果を表示し、ユーザーが保存前にそれを閲覧できるように、演習5.3で貴方が作ったプログラムを修正しなさい。
- 適用される変換をユーザーが選択できるように、RadioItemsでダイアログを開くメニュー関数を、演習5.6のプログラムに含めなさい。
- ScrollingListでファイル表示に使用するフォントをユーザーが選択できるように、ファイル表示プログラムを修正しなさい。
- 入力ダイアログを経由して、ユーザーが設定できる期間後に自動的に次のページにスクロールするタイマを、演習5.7のプログラムに含めなさい。
- 関数GetCurrentTimeと、毎分に時間と分による時間を表示するタイマを使用して、既存のプログラムを拡張しなさい。時間を表示するには貴方自身の方法を選択しなさい。つまり、言葉か、I/OモジュールdeltaPictureの描画関数を使用した良いピクチャとして。
- (大演習)キーボード及びマウス関数を拡張することで、編集能力に関して、ファイル表示プログラムを拡張しなさい。演習5.6、5.8と5.9の結果を組み合わせて、それを自分のウィンドウベースのエディタに拡張しなさい。
- 描画中にシフトキーが押されると、水平及び垂直線のみを描くことができるように、線描画プログラムを変更しなさい。線描画は、開始点と現在のマウス位置を接続する線のベストフィットであるべきである。
- 線の太さをサブメニューから選択できるように、線描画プログラムを拡張しなさい。
First Uploaded : July 21, 2002
Last Modified : July 28, 2003
Back