「The Icon Programming Language」 というプログラミング言語について調べてみます。 例によってその日に知ったことを書く、というスタイルで書いているので、 間違いも多いかと思います。指摘大歓迎です。
Icon Handbook というPDFがまとまっていて良さそうなので、これを参考に色々書きます。また、 風つかいさんのIcon講座、 という日本語の解説サイトがありました。こちらも参考になりそうです。
ホームページから、 Icon 9.3 for Windows をダウンロード。解凍。インストール。
スタートメニューからは、WindowsのGUI アプリとして体裁の整った開発環境が起動します。 ソースを打ち込んで、メニューから「Run」すれば動く、と。 ただ、常に画面の一番左上で起動するのが好きになれないので、 私はコンソール版を使うことに決めました。 NTICONT.EXE というのが処理系らしいので、icont.exe と改名して利用。
procedure main ()
write( "Hello, World" );
end
>> 実行結果 Hello, World
動いたー。procedure、で手続きを書く。endで終わり。 main手続きの中身が一番最初に実行される。
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以外の手続きを書いて、呼び出すこともできます。|| は文字列の連結演算子。
…と、ここまでのところは、どの手続き型言語にもありそうな特徴だけが出てきました。 次回に続く。^^
名前しか知らなかったこの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のどれかで割り切れてしまったら失敗… という処理を表しています。
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 を使った分は同じ意味になっています。
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 から何か結果を戻すことも可能です。
さて、またここで 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番目の引数で刻み幅を指定できる」という機能もあるので、付けてみました。
色々文字列処理ができます。とりあえず面白いのが、添え字の扱い。
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で置き換える)など、
よく使いそうな処理が一通り揃っています。
この文字は数字か?とか小文字か?とかを判定するための機構として、 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個だったら失敗、となります。「数字または小文字」みたいな条件を表すには、
++
演算子で繋ぎます。二つの共通部分を表す **
や片方にあってもう片方に無いものを表す --
などの演算子もあるようです。
例その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>
お久しぶりです。^^; 今回は、Icon 言語に組み込みのデータ構造について。 と言っても、文字列やリスト、文字集合はすでに扱ったので、それ以外を紹介します。
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できるので覚えておくと良さそうです。
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 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 について。
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的なもの、を変数に溜めておける、というのが一つ。 次回以降でもっと詳しい使い方を見ていこうと思います。
次の例を考えます。@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 という記事の下の方の流れ図がわかりやすいのでご一読を。
この機能はどういう風に便利なの?という点については、
こんなページ がありました。曰く、
「対戦型ゲームを自然にプログラムしようとすると、
コンテクストを保存した上でプレーヤに対応するサブルーチン間で
制御を渡し合える仕組みが欲しくなる。
」とのこと。なるほど。
というわけでせっかくなので対戦型ゲームをプログラムしてみましょう。 将棋や碁やらは面倒なので、○×ゲーム。
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
と、まあ思考ルーチンが全然思考してないのを気にしないことにすれば、 割と上手いこと書けました。
主に every
によるバックトラッキング、及び create
によるコルーチンの話題を中心に据えてIconという言語を眺めてみました。
いかがでしたでしょう? returnによってルーチンを「終了」させるのではなく、
suspendによって「中断」させてまたいずれその場所から再開するという考え方は、
JavaやCなどの一般的な言語でプログラムを書いた場合とは、
少し違っていてなかなか興味深いところです。専門的に言うと「context」
という概念らしいですが、私も詳細は知りません。
私の趣味の都合上言語そのもの的な特徴にしか触れていませんが、 Icon 自体はPerl的なテキスト処理にも使えたり、 GUIライブラリを標準で備えていたりと、汎用言語としても便利なヤツだそうです。 興味を持たれた方は実際に色々書いてみてもよさそうですね。
ではでは。