Icon Days

The Icon Programming Language」 というプログラミング言語について調べてみます。 例によってその日に知ったことを書く、というスタイルで書いているので、 間違いも多いかと思います。指摘大歓迎です。

Icon Handbook というPDFがまとまっていて良さそうなので、これを参考に色々書きます。また、 風つかいさんのIcon講座、 という日本語の解説サイトがありました。こちらも参考になりそうです。

  1. インストール
  2. 概観
  3. * every
  4. more on every
  5. * 手続き
  6. 文字列
  7. 文字集合
  8. データ構造色々
  9. co-expression
  10. * co-expression 2
  11. co-expression 3

インストール (2003/02/08)

ホームページから、 Icon 9.3 for Windows をダウンロード。解凍。インストール。

スタートメニューからは、WindowsのGUI アプリとして体裁の整った開発環境が起動します。 ソースを打ち込んで、メニューから「Run」すれば動く、と。 ただ、常に画面の一番左上で起動するのが好きになれないので、 私はコンソール版を使うことに決めました。 NTICONT.EXE というのが処理系らしいので、icont.exe と改名して利用。

hello, world


procedure main ()
	write( "Hello, World" );
end
>> 実行結果
Hello, World

動いたー。procedure、で手続きを書く。endで終わり。 main手続きの中身が一番最初に実行される。

概観 (2003/02/10)


procedure main ()
	writes( "あいうえお" )
	x := "かきくけこ"
	write( x )
end
>> 実行結果
あいうえおかきくけこ

write と違って writes では、後ろで改行せずに出力するらしい。 行末のセミコロンは無くてもいい(一行に2つの文を書きたいなら必要)。 変数名 := 値 で、変数を使える。特に宣言等は必要ない。


procedure main ()
	local x, y;
	x := 1+2;
	y := 2^x + 4;
	write( x );
	write( y );
end
>> 実行結果
3
12

local と書くと、ローカル変数になる。 + - * / などの算数は普通に書ける。^ は累乗。xorにあらず。


procedure main ()
	x := read()

	if x = 3 then
		write( "x was 3!" )
	else
		write( "x was not 3!" )

	while x > 0 do
	{
		writes(x)
		writes(x+5)
		x := x-1
	}
	write()

	y := read()
	case y of {
	  "1" : write("y was 1")
	  "2" : write("y was 2")
	  "3" : write("y was 3")
	  default: write("y was...?")
	}
end
>> 実行結果
3
x was 3!
382716
2
y was 2

if ~ then ~ else とか、 while ~ do ~ とか。 複数の文を書きたいところでは上のwhile~do~の例のように、{ } でくくります。 あと、Cでいうswitchが、case~of になるそうな。 あと、ループの途中でどこかに飛ぶ命令として、break、next (ソースは略)。


procedure main()
	write( decorate_string("lalala...") )
end

procedure decorate_string(s)
	return "*** " || s || " ***"
end
>> 実行結果
*** lalala... ***

main以外の手続きを書いて、呼び出すこともできます。|| は文字列の連結演算子。

…と、ここまでのところは、どの手続き型言語にもありそうな特徴だけが出てきました。 次回に続く。^^

every (2003/02/12)

名前しか知らなかったこのIcon言語について詳しく調べてみよう、 と私が思ったのは、このevery文を使ったサンプルを目にして興味を持ったから、 だったりします。こんな制御構文。


procedure main()
	every write( 1 to 5 )
end
>> 実行結果
1
2
3
4
5

every で修飾された中に 1 to 5 のような、 複数個の値を表すような式(generatorと言います)があると、 そのそれぞれの値について式の実行が行われます。もう少し複雑な例…


procedure main()
	every writes( (1 to 5) * (5 to 8) || " " )
end
>> 実行結果
5 6 7 8 10 12 14 16 15 18 21 24 20 24 28 32 25 30 35 40

まず左を1として、次に右を5から8まで回す。次に左を2として、 右を5から8まで回す………最後に左を5として、右を5から8まで。 という順序で全パターンでwritesされています。

 |
 |--------+--------+--------+--------+
 |        |        |        |        |
 1        2        3        4        5
 |-+-+-+  |-+-+-+  |-+-+-+  |-+-+-+  |-+-+-+
 | | | |  | | | |  | | | |  | | | |  | | | |
 5 6 7 8  5 6 7 8  5 6 7 8  5 6 7 8  5 6 7 8

この様な可能性の木を、左下から順番にたどって全パターン、 という動作をするそうです。toが3つ以上あっても同じように。


procedure main()
	every i := 10 to 20 by 2 & write(i*i)
end
>> 実行結果
100
144
196
256
324
400

10 to 20 by 2 で、10,12,14,16,18,20 という2個飛びで値が生成されます。 あと、& で文を繋ぐことで、複数をつなげて実行できます。

& で繋ぐ途中に条件式を挟むと、条件が成り立たなければそこで終了し、 次の値を取りに行く、と言ったことができます。


procedure main()
	every i:=seq(1) & i%3=0 & write(i)
end
>> 実行結果
3
6
9
12
.
.
.

seq(1) は、1,2,3,4,5,... と無限に値を生成し続けます。 従って、everyの中に単にseq()を入れると無限ループです。 このような無限generatorの使い道については後ほど。

上の例では、まず i に数を入れて、次にiを3で割ったあまりが0かどうか調べ、 0なら右に進んでwrite(i)、非0なら次のiを試す、という動作をします。 で、結局、3の倍数だけが順にwriteされていく、と。


procedure main()
	every i:=seq(1) & write(i) & i>=5 & break
end
>> 実行結果
1
2
3
4
5

everyループから抜けるのにも、whileから抜けるのと同じで break が使えます。 every自体を条件判断につかうこともできます。普通に終了したeveryはfail、 breakで抜けたeveryはsuccessを表すそうなので、


procedure main()
	if (every ... break) then {
		breakで抜けた場合
	} else {
		everyで全要素実行した場合
	}
end

てなことができます。


procedure main()
	every i:=seq(2) & not(every i%(2 to i-1)=0 & break) & write(i)
end
>> 実行結果
2
3
5
7
11
.
.
.

と、今日の成果をまとめて一つ、少し複雑なモノを書いてみました。 2以上の数iについて、2~i-1のどれかで割り切れてしまったら失敗… という処理を表しています。

more on every (2003/02/15)


procedure main()
	every writes( 5|4|3|2|1 )
	write()
	every writes( seq() \ 9 )
	write()
end
>> 実行結果
54321
123456789

縦棒 | で複数の数を区切って並べると、その並べた要素を順に generate させることができます。バックスラッシュ(フォントによっては¥記号に見えるかも) のあとに数字を続けると、「9個まで」みたいに回数制限をつけることができます。


procedure main()
	every writes( |2 \100 )
	write()
end
>> 実行結果
22222222222222222222222222222222222222222222222222222222222222222222222222
22222222222222222222222222

縦棒 | を頭につけると、その右の値を無限に生成し続けます。


procedure main()
	x := list(3)
	x[1] := "abc"
	x[2] := "def"
	x[3] := "ghi"

	every write( ! x )
	every write( x[1 to *x] )
end
>> 実行結果
abc
def
ghi
abc
def
ghi

iconには list、というデータ型があります。Cとかで言う配列のことみたいですが。 listの各要素について実行するには、! を頭につけてeveryします。 * はリストの長さを返す演算子なので、上の例では、! を使った文と、 1 to *x を使った分は同じ意味になっています。

手続き (2003/02/15)


procedure main()
	aaa() # ここでaaaを呼び出し
end

procedure aaa()
	write( "abc" )
end
>> 実行結果
aaa

上のように、procedure ~ end で、main 以外の手続きも作ることができます。 特に驚くようなところはありません。


procedure main()
	(aaa|bbb) () # 最初にaaa、次にbbbを呼び出し
end

procedure aaa()
	write( "aaa" )
end

procedure bbb()
	write( "bbb" )
end
>> 実行結果
aaa
bbb

呼び出すprocedureの名前部分は、何も書かなくても every がついているかのような扱いになります。ちょっとびっくりです。 一つのデータに対して、色んな処理を連続で適用することができるわけですね。


procedure decorate_string(s)
	return "*** " || s || " ***"
end

procedure main()
	write( decorate_string("lalala...") )
end
>> 実行結果
*** lalala... ***

最初の方に書いた例の再掲ですが、return を使うと、 procedure から何か結果を戻すことも可能です。

suspend

さて、またここで every がらみの話題に戻りましょう。^^; なんと、自分で generator を書いてみます。


procedure calc(x,y)
	suspend x+y
	suspend x-y
	suspend x*y
	suspend x/y
end

procedure main()
	every i := calc(12,3) & write(i)
end
>> 実行結果
15
9
36
4

これには、return とよく似た、suspend という文を使います。

まず最初にcalcが呼ばれると、suspend x+y に来て x+y を計算し、 mainへ戻ります。で、その結果をiに入れて、write。頭にeveryが付いているので、 iconは、次の値を生成しようとします。次の値の計算のために、さっき suspend で「中断」した次の文からcalcの実行が再開されます。次は suspend x-y なので、x-y を計算し、main へ戻ります。以下同様。

結構シンプルにgeneratorが作れることがわかりましたので、 標準で用意されていたgeneratorと同じものを、自分で書いてみたくなります。 狙いは seq()。与えられた数から始まる数字の列を生成するgeneratorです。


procedure myseq(x)
	repeat {
		suspend x
		x +:= 1
	}
end

procedure main()
	every write( myseq(2) \ 5 )
end
>> 実行結果
2
3
4
5
6

できました。repeat、という無限ループ文を使っています。


procedure myseq( i, s )
	/i := 1 # /i で、iが&nullなら i、そうでなければこの文はfailする
	/s := 1 # ので、省略されてたら iやs に 1 をセット、という意味になる
	repeat { suspend i; i+:=s }
end

procedure main()
	# (,2) のように引数を省略すると、&null という値が自動で渡される
	every write( myseq(,2) \ 5 )
end
>> 実行結果
1
3
5
7
9

標準のseq()には、「引数を省略したら 1 からスタート」とか 「2番目の引数で刻み幅を指定できる」という機能もあるので、付けてみました。

文字列 (2003/02/16)

色々文字列処理ができます。とりあえず面白いのが、添え字の扱い。


procedure main()
	s := "hello"
	write( s[2] )   # 2番目の字を表示
	write( s[2:4] ) # 2番目の字から4番目の字の手前まで表示
	s[2:4] := "ABC" # そこに代入もOK
	write( s )
end
>> 実行結果
e
el
hABClo

1以上の数字を使うと、文字列の先頭から何番目、という意味になります。 逆に-1以下の数字を使うこともできます。


procedure main()
	s := "hello"
	write( s[-2] )    # 後ろから2番目の字を表示
	write( s[-4:-2] ) # うしろから4番目の字から後ろから2番目の字の手前まで表示
	s[-4:-2] := "ABC" # そこに代入もOK
	write( s )
end
>> 実行結果
l
el
hABClo

厳密に言うと、添え字は「文字」を指すのではなくて字と字の「間」を指すそうです。

1  2  3  4  5  6
V  V  V  V  V  V
 h  e  l  l  o

hとeの間が2で、eとlの間が3、と。確かにこう考えると、 上の例と結果があっていますね。また、右端から逆順にも番号がふられています。

   h  e  l  l  o
 ^  ^  ^  ^  ^  ^
-5 -4 -3 -2 -1  0

これを使うと、次のような書き方ができます。「先頭以外の全部」と 「末尾以外の全部」を表す方法。上の添え字の位置の図を睨んでみると、 確かにそういう意味になっていることがわかるかと思います。


procedure main()
	write( "hello"[2:0] )
	write( "hello"[1:-1] )
end
>> 実行結果
ello
hell

あと、文字列に関する基本的な手続きが幾つかあります。left (左から何文字かとってくる。だいたい s[1:n] と同じ)とか center とか right とか。char(文字コードの数値から文字を作成)とか。 replace(s1,s2,s3) (s1の中のs2をs3で置き換える)など、 よく使いそうな処理が一通り揃っています。

文字集合 (2003/02/16)

この文字は数字か?とか小文字か?とかを判定するための機構として、 character set というものがあります。


procedure main()
	s := "aiueo"
	if any( &digits, s ) then
		write( "数字発見" )
	else
		write( "数字はないよ" )

	c := &digits ++ &lcase

	if any( c, s ) then
		write( "数字もしくは小文字発見" )
	else
		write( "数字も小文字もないよ" )
end
>> 実行結果
数字はないよ
数字もしくは小文字発見

any( 文字集合, 文字列 ) で、一個でもその種類の文字が見つかれば成功、 0個だったら失敗、となります。「数字または小文字」みたいな条件を表すには、 ++ 演算子で繋ぎます。二つの共通部分を表す ** や片方にあってもう片方に無いものを表す -- などの演算子もあるようです。

every/文字列 の応用例

例その1。文字列から、最後の \\ より後ろを取り出します。 Windowsで絶対パスからファイル名を取り出す時に使える作業です。 (ただし、2byte文字には対応していませんが。^^;) find(a, b) が a の位置を b の中から検索して返す、という手続きですが、 こいつは同時にgeneratorでもあります。つまり、2回以上呼び出すと、 a の2番目、3番目…の出現を探しに行きます。一度も見つからなかったら failして、every文全体が終了します。


procedure main()
	s := "c:\\test\\directory\\filename.txt"

	# findがfailしなかったら結果をiに代入。
	# 結局 \ が最後に現れる位置が i に入って終わる。
	i := 0
	every i := find( "\\", s )
	write( s[i+1:0] )
end
>> 実行結果
filename.txt

例その2。恐ろしく効率が悪いコードですが気にしないでください。 サンプルのソースを手でHTMLに直すのに疲れたKさん(仮名) がタグ付けを自動化しようとしたようです。文字列を2行に続けたいときは、 下線(_)でつなげます。あと、every~doは見たままの意味。


link strings

procedure main()
	s := "procedure main()\n  _
	        write(12345)\n_
	      end"

	keywords := ["if", "then", "else", "every", "procedure", "end"]
	every k := !keywords do
		s := replace(s, k, "<span class=\"kwd\">"||k||"</span>")

	write( s )
end
>> 実行結果
<span class="kwd">procedure</span> main()
  write(12345)
<span class="kwd">end</span>

データ構造色々 (2003/02/24)

お久しぶりです。^^; 今回は、Icon 言語に組み込みのデータ構造について。 と言っても、文字列やリスト、文字集合はすでに扱ったので、それ以外を紹介します。

table


procedure main()
	t := table()
	t["aaa"] := "あああ"
	t["iii"] := "いいい"
	t["uuu"] := "ううう"

	every write( !t )
	write( "-----" )
	every write( key(t) )
	write( "-----" )
	every k:=key(t) do write( k || " -> " || t[k] )
end
>> 実行結果
ううう
あああ
いいい
-----
uuu
aaa
iii
-----
uuu -> ううう
aaa -> あああ
iii -> いいい

tableこと、連想配列。キーは文字列以外でも使えます。基本的にどのデータ型でも、 ! で全要素をgenerateできるので覚えておくと良さそうです。

set


procedure main()
	s := set()
	insert(s, 123)
	insert(s, 456)
	insert(s, 789)
	t := set()
	insert(t, 987)
	insert(t, 456)
	insert(t, 321)
	u := s ++ t # 和集合
	every write( !u )
end
>> 実行結果
123
987
321
789
456

set。集合。csetこと文字集合の一般バージョンです。

record


record Point(x,y) # recordの宣言

procedure main()
	t := Point(3,4) # インスタンス作成

	write( p.x || " " || p.y )
	write( "----" )
	every write( !p )
	write( "----" )
	p.x := 100
	write( p["x"] + p["y"] )
end
>> 実行結果
3 4
----
3
4
----
104

record。C言語で言う構造体みたいなものです。.(ドット) でアクセスできるのも全く同じ。注目すべきは、recordでも ! で 全フィールド列挙ができるのと、[ ] に文字列を与えてのアクセスも 可能なことでしょうか。ECMAScriptがちょっとこれに似てるなぁ、と思ったりしました。

co-expression (2003/03/04)

日本語だとなんて訳すんだろう? co-expression について。


procedure main()
	c := create 1 to 5 # co-expression "1 to 5" を作る。
	while i := @c do write( i ) # @ でco-expressionを一回評価
end
>> 実行結果
1
2
3
4
5

…というものだそうです。これ、generator を every で回すのとの違いは何でしょう? ぱっと見、generator的なもの、を変数に溜めておける、というのが一つ。 次回以降でもっと詳しい使い方を見ていこうと思います。

co-expression 2 (2003/03/08)

次の例を考えます。@coexp で co-expression を起動するのはさっきの例と同じですが、今度は @ が2項演算子として登場します。 その意味については、コメントで。


procedure main()
	c1 := create hello()
	c2 := create goodbye(c1)
	@c2 # mainルーチンから c2 へ制御を移す
end

procedure hello()
	i := 1
	while i < 100 do
	{
		write( "hello" || i )
		i := (i+1) @ &source
		  # 自分の値をi+1として、呼び出し元(&source)の
		  # コルーチンへ制御を移す。その&sourceの戻り値をiへ代入
	}
	101 @ &source
		  # 自分の値を101として、呼び出し元(&source)の
		  # コルーチンへ制御を移す
end

procedure goodbye(coexp)
	i := @coexp
	  # 引数として与えられたco-expressionへ制御を移す
	while i < 100 do
	{
		write( "goodbye" || i )
		i := (i+2) @ &source
		  # 自分の値をi+2として、呼び出し元(&source)の
		  # コルーチンへ制御を移す。その&sourceの戻り値をiへ代入
	}
	101 @ &source
		  # 自分の値を101として、呼び出し元(&source)の
		  # コルーチンへ制御を移す
end
>> 実行結果
hello1
goodbye2
hello4
goodbye5
hello7
goodbye8
hello10

..(中略)..

goodbye89
hello91
goodbye92
hello94
goodbye95
hello97
goodbye98

まずmainから実行開始
→ 中で2つのco-expressionを作成
→ c2に制御を移す
→ c2の中身であるgoodbye(c1)が実行される
→ goodbyの中でまずcoexpつまりc1に制御が移る
→ hello手続きが呼び出され実行が進む
(i+1) @ &sourceでは、自分の値をi+1(この場合、2)として 現在のco-expressionの実行を一旦中断し、呼び出し元であるc2に戻る
→ この時戻ったgoodbye側では、先ほどの@coexpという式の評価結果が2となり、 その続き、つまりiに値2を代入する作業から再開
→ そのままgoodbyeの実行が進み (i+2) @ &source に動作が 到達すると、現在のco-expressionの実行を一旦中断し、 自分の値をi+2(つまり4)として、呼び出し元であるc1に戻る
→ この時戻ったhello側では、先ほどの(i+1) @ &source という式の評価結果が4となり、その続き、つまりiに4を代入してループの 実行を続けるところから再開

…ということになっています。(めっちゃわかりにくい説明でスミマセン^^;) 普通のサブルーチン呼び出しだと、呼ばれたサブルーチンはかならず最初から 始まり最後やreturnで抜けて終わっていくことになりますが、co-expression によるコルーチンでは、コルーチンを呼び出すとさっき中断したときの続きから再開し、 また適当に途中で中断して戻る、という動作になります。

Co-expressions in Icon という記事の下の方の流れ図がわかりやすいのでご一読を。

この機能はどういう風に便利なの?という点については、 こんなページ がありました。曰く、 「対戦型ゲームを自然にプログラムしようとすると、 コンテクストを保存した上でプレーヤに対応するサブルーチン間で 制御を渡し合える仕組みが欲しくなる。」とのこと。なるほど。

co-expression 3 (2003/03/17)

というわけでせっかくなので対戦型ゲームをプログラムしてみましょう。 将棋や碁やらは面倒なので、○×ゲーム。


procedure sente( ai )
	# 初期盤面
	brd := [ [0,0,0], [0,0,0], [0,0,0] ]
	# 0な要素がある間ループ
	while brd[1 to 3][1 to 3] == 0 do
	{
		brd := ai( brd, 'O' )      # 思考ルーチンで考える
		print_brd( brd )           # 盤面表示
		win_lose_check( brd, 'O' ) # 勝ち負け判定
		brd := brd @ &source       # 現在の盤面を相手(&source)へ渡す
	}
end

procedure gote( c, ai )
	# 一手目は相手が打つ
	brd := @c
	# 0な要素がある間ループ
	while brd[1 to 3][1 to 3] == 0 do
	{
		brd := ai( brd, 'X' )      # 思考ルーチンで考える
		print_brd( brd )           # 盤面表示
		win_lose_check( brd, 'X' ) # 勝ち負け判定
		brd := brd @ &source       # 現在の盤面を相手(&source)へ渡す
	}
end

procedure print_brd( brd )
	# 盤面表示
	every i := 1 to 3 do {
		every j := 1 to 3 do
			if brd[i][j] == 0 then
			  writes('.')
			else
			  writes(brd[i][j])
		write()
	}
	write()
end

procedure win_lose_check( brd, t )
	# 勝ち負け判定。手抜き。
	every i := 1 to 3 do {
		if t == brd[i][1] == brd[i][2] == brd[i][3] then
			{ write(t||" is the winner"); exit(0) }
		if t == brd[1][i] == brd[2][i] == brd[3][i] then
			{ write(t||" is the winner"); exit(0) }
	}
	if t == brd[1][1] == brd[2][2] == brd[3][3] then
		{ write(t||" is the winner"); exit(0) }
	if t == brd[3][1] == brd[2][2] == brd[1][3] then
		{ write(t||" is the winner"); exit(0) }
end

#################

procedure AI_1( brd, t )
	# 左上から順に打てるところに打っていく。物凄い手抜き。
	# お暇な方はここにまともな思考ルーチンを…
	every i := 1 to 3 & j := 1 to 3 &
		brd[i][j]==0 & brd[i][j]:=t & return brd
end

procedure AI_2( brd, t )
	# 右上から順に打てるところに打っていく。物凄い手抜き。
	# お暇な方はここにまともな思考ルーチンを…
	every i := 1 to 3 & j := 3 to 1 by -1 &
		brd[i][j]==0 & brd[i][j]:=t & return brd
end

procedure main()
	# 実行。先手はAI1号で、後手はAI2号で。
	p1 := create sente(AI_1)
	p2 := create gote(p1, AI_2)
	@p2
end
>> 実行結果
O..
...
...

O.X
...
...

OOX
...
...

OOX
..X
...

OOX
O.X
...

OOX
OXX
...

OOX
OXX
O..

O is the winner

と、まあ思考ルーチンが全然思考してないのを気にしないことにすれば、 割と上手いこと書けました。

まとめ (2003/03/17)

主に every によるバックトラッキング、及び create によるコルーチンの話題を中心に据えてIconという言語を眺めてみました。 いかがでしたでしょう? returnによってルーチンを「終了」させるのではなく、 suspendによって「中断」させてまたいずれその場所から再開するという考え方は、 JavaやCなどの一般的な言語でプログラムを書いた場合とは、 少し違っていてなかなか興味深いところです。専門的に言うと「context」 という概念らしいですが、私も詳細は知りません。

私の趣味の都合上言語そのもの的な特徴にしか触れていませんが、 Icon 自体はPerl的なテキスト処理にも使えたり、 GUIライブラリを標準で備えていたりと、汎用言語としても便利なヤツだそうです。 興味を持たれた方は実際に色々書いてみてもよさそうですね。

ではでは。

presented by k.inaba   under NYSDL.