プログラミング言語 Io
Io言語(以下単にIo)は大変魅力的なプログラミング言語だが日本語による情報がほとんどない。そこで、過去ぼくが書いた Io についての記事をここの掲載しておこうと思う。これを書いた時期にはこの Io と Haskell を勉強していたが、現在はどちらも封印している。ウェブとロボットに集中するためにぼくの趣味の一つであるプログラミング言語についての探求を棚に上げたというわけだ。しかしせっかく書いた記事でもあり、Io に興味のある人たちの参考になるとも思いここに再掲載することにしたのだ。
【 Io Language - http://iolanguage.org/ 】
Io は JavaScript のごとくプロトタイプベースで Smalltalk のごとく純粋オブジェクト指向の言語。誕生したのは 2002年4月とのこと。現在満11歳の言語だ。この Io はメッセージ・パッシングという単一機構にこだわった単純明快な言語のようだ。高階関数をもち再帰が可能で、並行処理のためのコルーチン、アクター、フューチャをもつ。また、ガベージコレクションも備えている。
ところで、Bruce A. Tate という人の書いた本「Seven Languages In Seven Weeks」で、7つの言語:Ruby、Io、Prolog、Scala、Erlang、Clojure、Haskell が取り上げられている。Io と Prolog を除く他の5つは現在最も注目される言語たちといってよいであろう。これらに比べると特に Io の知名度はかなり低いのではないか。しかし、Io は他の6つにに負けないほど魅力的かもしれない。少なくともぼくは他の言語以上に注目しているのだ。
どんなゲームが好きかで言語の好も想像できる。お膳立てやイベントがたくさんあって、それに沿っていけばエンディングにたどり着き、そこでボスキャラと一騎打ち。それも悪くはないだろう。しかし、ぼくは小さなルールだけがあって多くの村や町を自由に行き来するゲームが好きだ。言語でいえば、それは Io ということになる。
最近、Io の公式サイト(http://iolanguage.com/)をよく見ているが、全体的にドキュメントが不親切だ。英文でいいから(当たり前か)リファレンスなどの解説を増やしてもらいたいものだ。Io の英語版の書籍がないものかと探したが見当たらない。出る予定もないんだろうなぁ、ソースコードを見ればいい? ぼくにそんな根性はない。まぁ、でも少しずつ分かってきたので、今回、少し紹介しようと思った次第。(ぼくの Io 学習は 2012年5月20日の日曜日に始まった)
この言語は、コマンドプロンプトであれこれ実験するのがとても楽しい。まるで新しいゲームを手に入れてそれにハマってしまったような状態なのだ。今回はその結果を紹介したいと思っているのだが、ぼく自身 Io の初心者であり、間違ったことをだいぶ書いてしまうかもしれない。あらかじめご了承いただき、間違っている点はご教授などいただければ幸いです。
さて、まずは公式サイトから Windows版を手に入れよう。Ubuntu の人も Windows版がおすすめだ(ま、冗談)。ぼくの場合は、C:\Io に放り込んで、C:\Io\bin にパスを通した。ただそれだけ。
Io のためのエディタとしては、現在、Mery を使っているが、jEdit(http://www.jedit.org/)というエディタが Io の構文に対応しているということでインストールしてみた。
で、こんな感じで、カラー表示。ちょっと古くて対応していないキーワードもある模様。このくらいなら自前で作ってもいいかも。興味がわいた人は使ってみてください。さてさて、始めよう。まずはシェル。コマンドプロンプトから io と打ち込めばシェルが立ち上がる。
C:\>cd io C:\Io>io Io 20110905 Io>
すると、Io のバージョンを表示して入力待ちとなる。おきまりの Hello World をやってみよう。
Io>"Hello World" print Hello World==> Hello World Io>exit C:\Io>
最初の Hello world は print の結果。次の Hello World は結果の値。
exit で抜ける。シェルの話はこれで終わり。いろいろ自由にやってみてほしい。
おっと、ここで、使用する前に Io のコメントについて。次の通り3種類ある。# は先頭行で用いれば Unix 世界の意味を持つ。(詳細は省略)
// コメント # コメント /* コメント */
プログラムファイルのエンコードは、日本語が使いたければ UTF-8N(BOM なし)がよいだろう。それで次のようにちょっと試してみたのだ。(右端が切れてしまったがスクロールが可能)
"" println "// ダメ文字といわれる漢字を表示してみる" println "// ―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭" println s := "―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭" s println "\n" println "// その encoding を表示してみる" println "―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭" encoding println "\n" println "// 先頭に a を挿入してみる" println "a―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭" println "\n" println "// ヒアドキュメントで先頭に改行を挿入してみる" println h := """ ―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭 ―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭 ―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭 """ println "\n" println "// UTF-8 を指定する" println u := "―ソЫ噂浬欺圭構蚕十申曾箪貼能表暴予禄兔喀媾彌拿杤歃濬畚秉綵臀藹觸軆鐔饅鷭" asUTF8 u println "\n" println "// その encoding は" println u encoding println "\n" println "// 指定しない場合の interpolate" println "#{s}" interpolate println "\n" println "// UTF8 の場合の interpolate" println "#{u}" interpolate println "\n" println
結果は次の通り。
コマンドプロンプトのコードページはご覧のように 65001 に設定して行った。結果は、日本語文字列の1文字目が化けてしまう。1文字目に半角(スペースや改行もOK)を挿入すると正常となる。まぁ、これは何とか対処できそうだ。受け入れるとしよう。これ、最初からおもしろくない話をしていると思うので、この辺りのこと、あとはお任せする。
さて、それでは、ありがちな普通の話に戻るとしよう。
Io言語は、シーケンス、リスト、マップなどのコレクションとそれぞれに豊富なメソッドをもっている。でも、それを一々書くのは退屈なので次の機会にでも譲りたい。でも、制御構造については紹介したい思う。
// // Io の Control Flow // if(y < 10, x := y, x := 0 ) if(y < 10) then(x := y) else(x := 0) // こちらの形式も可 switch( n==1, write("n=1"), n==2, write("n=2"), n==3, write("n=3") ) loop( write("Endless") ) 100 repeat( write("100 Times") ) a := 1 while(a < 10, a println a = a + 1 ) for(i, 0, 10, 1, i println ) list("abc", "def", "ghi") foreach(println) // List の foreach aMap foreach(k, v, writeln(k, " = ", v)) // Map の foreach
以上、見たとおりだが、switch はぼくが作ったものだ。次のように定義した。
// // Io に新たな制御構造 switch を自前で定義 // switch := method( ac := call message argCount for(i, 0, ac-1, 2, (call sender doMessage(call message argAt(i))) ifTrue( call sender doMessage(call message argAt(i+1)) break) ) ) // // switch のテスト // n := 4 switch( n==1, writeln("n=1"), n==2, writeln("n=2"), n==3, writeln("n=3"), n==4, writeln("n=4"), /* ここで一致、n=4 を表示して break する */ n==4, writeln("n=4"), /* この行も同じ条件を満たすが実行されない */ n==5, writeln("n=5") ) # 実行結果 ==> n=4
次は Io のオブジェクト指向について。Io のオブジェクト指向は JavaScript どうようにプロトタイプベースだ。クラスは存在しない。
// // オブジェクト // Account := Object clone // 口座オブジェクトを生成(複製) Account balance := 0 // 残高スロットの初期化 Account deposit := method(v, balance := balance + v) // 入金 Account withdraw := method(v, balance := balance - v) // 出金 Account show := method( write("Account balance: ", balance, "\n") /* 残高表示 */ ) myAccount := Account clone // 自分専用口座オブジェクトを生成 myAccount deposit(10000) // 10,000円入金 myAccount show // 残高の確認
上の Object はルートレベルのオブジェクト。
// // オブジェクトのタイプ // Pen := Object clone BallpointPen := Pen clone myBallpointPen := BallpointPen clone Pen type #==> Pen BallpointPen type #==> BallpointPen myBallpointPen type #==> BallpointPen
上の例のように小文字で始まるスロットは親プロトタイプのタイプをもつ。
// // Singleton // # MyObject の clone は不可となる MyObject := Object clone MyObject clone := method(return self)
上は、MyObject を clone しようとしても自分自身を返すため、複製されることのないオブジェクトとなる。
// // Overriding // Person := Object clone Person name := "Slump" Person title := method(writeln(name)) Doctor := Person clone Doctor title := method(write("Dr. ") resend) Doctor title println #==> Dr. Slump
上は、resend によって Doctor の親オブジェクト Person にメッセージを送ることで、Person のコンテキストを使用している。
Io は再帰も可能だ。今どきじゃ、当たりまえか。
// // 再帰 // factorial := method(n, if (n == 1, return 1, return n * factorial(n - 1) ) ) factorial(10) println #==> 3628800
次はフィボナッチ数列をやってみよう
// // フィボナッチ数列 // fib := method(i, if(i < 3, 1, fib(i -2) + fib(i - 1))) // // fib(1) から fib(35) まで表示する // for(n, 1, 35, "fib(#{n}):" interpolate alignLeft(9, " ") print fib(n) println )
次はコルーチンを見てみよう。
// // Coroutine // fib := method(i, if(i < 3, 1, fib(i -2) + fib(i - 1))) o1 := Object clone o1 fibo1 := method(m, n, for(i, m, n, 1 "o1 fib(#{i}):" interpolate alignLeft(12, " ") print fib(i) println; yield ) ) o2 := Object clone o2 fibo2 := method(m, n, for(i, m, n, 1 "o2 fib(#{i}):" interpolate alignLeft(12, " ") print fib(i) println; yield ) ) o1 @@fibo1(1, 30) o2 @@fibo2(1, 30) Coroutine currentCoroutine pause
上の例が平行性として意味のあるプログラムになっているかという点についてはご容赦願いたい。見かけ上は協調して動いているような結果にはなった。一応解説しよう。yield はこれに出会うたびにもう一つのプロセスに制御を移す。また @@fibo1 のように @@ を付けることで非同期なメッセージ送信となる。
似ているようだがもう一つ。Io のもつフューチャについてだ。
// // future // fib := method(i, if(i < 3, 1, fib(i -2) + fib(i - 1))) futurefib := @fib(31) // ここで fib(31) を @ 付きで呼び出している for(n, 1, 30, /* ここで fib(1)~fib(30) までを計算している */ "fib(#{n}):" interpolate alignLeft(9, " ") print fib(n) println ) "fib(31): " print; futurefib println // futurefib にアクセス
前の例では @@ だったが、こんどは @ を用いている。同じ非同期送信ではあるが、@@ はすぐに nil を返すが @ はフューチャを返す。フューチャは自身のプロセスが終了した時点で本来の値を返す。 もし、本来の値が返される前にフューチャにアクセスした場合は、返されるまでブロックされる。
次にファイルの入出力をやってみた。
// // file1 // fio := File with("in.txt") openForReading allin := fio contents fio close allout := allin foo := File with("out.txt") openForUpdating foo write(allout) foo close
上は、あるテキストファイルを全部を読み込んで、別ファイルに全部そのまま書き込むテストだ。やってみると出力ファイルに改行が2重に書き込まれてしまう。改行コードの問題だろう。試しに入力ファイルの改行コードを LF にしたらうまくいった。うん、うまくはないか。で、次。
// // file2 // fio := File with("in.txt") openForReading allinList := fio readLines fio close allin := allinList join("\n") allout := allin foo := File with("out.txt") openForUpdating foo write(allout) foo close
上だが、今度は1行ずつ読み込みリストに格納。それをつないでから書き込んだ。これならと思いテストしたが、結果はうまくいった。うーん、簡単な例ではあるが、ぼくに何か見落としがあるのか..もとは Unix だからね。まぁ、いいか。
シーケンスとリスト、マップをサボったけど、ずいぶん長く書いてしまった。今回はコマンドの引数について書いて最後にしよう。
// // args.io 引数を受け取るテストプログラム // myargs := System args println for(i, 0, 3, 1, myargs at(i) println) // コマンドプロンプトから // io args.io AAAAA BBBBB CCCCC // の実行結果 #==> list(args.io, AAAAA, BBBBB, CCCCC) #==> args.io #==> AAAAA #==> BBBBB #==> CCCCC
いえね、これ、簡単なことだけど、ぼくはすぐに気づかなかった。そういうわけで、ここで、ぼくどうようの「気づかない系」の人のためにお教えしようと思った次第。
さてさて、Io は文字列処理にも優れるように思う。リストやマップにも必要以上のメソッドがそろっている。それらについて今回は割愛したが、それらが気に入ったからこそ Io をもう少し使ってみようと思う。
ぼくにとって Io言語はおもしろくて神秘的だ。っていうことでも分かるように、ぼくには Io言語がまだよく分からない。もちろん、ここまで程度の理解で小さなプログラムなら実用的なものを書くこともできる。文字列処理やリスト処理辺りなら自由自在だ。この辺りの事情はすべてを理解しなくても使える JavaScript とどうようだ。しかし、Io言語の原理をすべて完全に理解したい。その辺りが一番おもしろいように思う。さて、ここからも思いついたまま気が向いたままに書きたいと思う。Io言語の学習を初めて10日ほどの初心者が書くことなので内容に関しては大目に見てほしい。
Io言語ではすべてのものがオブジェクトであり、すべてのアクションはメッセージなのだ。
これが Io言語がおもしろい一つの理由だ。
さて、見てのとおり Io のオブジェクトを作るのは簡単だ。
GameMachine := Object clone PlayStation := GameMachine clone
上では GameMachineオブジェクトをルートオブジェクトである Object を複製することで生成している。次に GameMachine を親にもつ PlayStation を生成。どうようにして次々と複製していく。
また、あるオブジェクトの protosリストには、proto をいくつでも追加でき、これによって多重継承を実現している。
myObject appendProto(otherObject)
次に protos と proto についてもう少ししらべてみよう。
上の図は Io言語のネームスペースについてのものだ。番号付き矢印を追っかけてみてほしい。
Lobby というのはこの図でも分かるようにネームスペースの元締めのような存在だ。また、図ではすべてのオブジェクトはプロトタイプへのリンクをもっており、自分の知らないメッセージを受けたときは protoリストを深さ優先でたどっていく。protos には proto の全リストが入っており、次のようにして知ることができる。
o := Object clone p := o protos p print
あるオブジェクトの protos の最初のリストが直前の親 proto のスロットリストになっているようだ。ルートオブジェクトである Object の場合は その protos と proto は同一のリストとなる。この辺りの表現は正確性を欠いているかもしれない。
次のリストはルートオブジェクトの protos を print した結果だ。つまり、すべてのオブジェクトは最低限これらのスロットが使用できるということになる。
// // protos // o := Object clone p := o protos p print #==↓ Object にはこれだけのスロットがある /* list( Object_0x3a4070: = Object_() != = Object_!=() - = Object_-() .. = method(arg, ...) < = Object_<() <= = Object_<=() == = Object_==() > = Object_>() >= = Object_>=() ? = method(...) @ = method(...) @@ = method(...) actorProcessQueue = method(...) actorRun = method(...) addTrait = method(obj, ...) ancestorWithSlot = Object_ancestorWithSlot() ancestors = method(a, ...) and = method(v, ...) appendProto = Object_appendProto() apropos = method(keyword, ...) argIsActivationRecord = Object_argIsActivationRecord() argIsCall = Object_argIsCall() asSimpleString = method(...) asString = method(keyword, ...) asyncSend = method(...) become = Object_become() block = Object_block() break = Object_break() clone = Object_clone() cloneWithoutInit = Object_cloneWithoutInit() compare = Object_compare() contextWithSlot = Object_contextWithSlot() continue = Object_continue() coroDo = method(...) coroDoLater = method(...) coroFor = method(...) coroWith = method(...) currentCoro = method(...) deprecatedWarning = method(newName, ...) do = Object_do() doFile = Object_doFile() doMessage = Object_doMessage() doRelativeFile = method(path, ...) doString = Object_doString() evalArg = Object_evalArg() evalArgAndReturnNil = Object_evalArgAndReturnNil() evalArgAndReturnSelf = Object_evalArgAndReturnSelf() for = Object_for() foreachSlot = method(...) futureSend = method(...) getLocalSlot = Object_getLocalSlot() getSlot = Object_getSlot() handleActorException = method(e, ...) hasDirtySlot = Object_hasDirtySlot() hasLocalSlot = Object_hasLocalSlot() hasProto = Object_hasProto() hasSlot = method(n, ...) if = Object_if() ifError = method(...) ifNil = Object_thisContext() ifNilEval = Object_thisContext() ifNonNil = Object_evalArgAndReturnSelf() ifNonNilEval = Object_evalArg() in = method(aList, ...) init = Object_init() inlineMethod = method(...) isActivatable = Object_isActivatable() isError = false isIdenticalTo = Object_isIdenticalTo() isKindOf = method(anObject, ...) isLaunchScript = method(...) isNil = false isTrue = true justSerialized = method(stream, ...) launchFile = method(path, args, ...) lazySlot = method(...) lexicalDo = Object_lexicalDo() list = method(...) loop = Object_loop() markClean = Object_markClean() memorySize = Object_memorySize() message = Object_message() method = Object_method() newSlot = method(name, value, doc, ...) not = nil or = true ownsSlots = Object_ownsSlots() pause = method(...) perform = Object_perform() performWithArgList = Object_performWithArgList() prependProto = Object_prependProto() print = method(...) println = method(...) proto = Object_proto() protos = Object_protos() raiseIfError = method(...) relativeDoFile = method(path, ...) removeAllProtos = Object_removeAllProtos() removeAllSlots = Object_removeAllSlots() removeProto = Object_removeProto() removeSlot = Object_removeSlot() resend = method(...) return = Object_return() returnIfError = method(...) returnIfNonNil = Object_returnIfNonNil() serialized = method(stream, ...) serializedSlots = method(stream, ...) serializedSlotsWithNames = method(names, stream, ...) setIsActivatable = Object_setIsActivatable() setProto = Object_setProto() setProtos = Object_setProtos() setSlot = Object_setSlot() setSlotWithType = Object_setSlotWithType() shallowCopy = Object_shallowCopy() slotDescriptionMap = method(...) slotNames = Object_slotNames() slotSummary = method(keyword, ...) slotValues = Object_slotValues() stopStatus = Object_stopStatus() super = method(...) switch = method(...) thisContext = Object_thisContext() thisLocalContext = Object_thisLocalContext() thisMessage = Object_thisMessage() try = method(...) type = Object_type() uniqueHexId = method(...) uniqueId = Object_uniqueId() updateSlot = Object_updateSlot() wait = method(s, ...) while = Object_while() write = Object_write() writeln = Object_writeln() yield = method(...) ) */
次に init、forward などをしらべてみよう。
init はコンストラクタに相当する特別なメソッドで、当該オブジェクトが複製されたときに実行される。次のリストで確認できるだろう。また、同じく次のリストで forward の挙動も確認できる。リストにある例では myMemo オブジェクトに hogehoge という存在しないメッセージを送ったとき forwardメソッドが起動されているが、もちろん、forward はそのようなためだけに存在するはずはなく、メッセージに応じて他のオブジェクトにメッセージを(分析、加工して)正に「転送」するためのものではないだろうか。
// // slot // Memo := Object clone do( ownerName := "Uninitialized" ownerPhone := "Uninitialized" ownerAddress := "Uninitialized" ownerMemo := "Uninitialized" init := method( /* init */ self ownerName := "Owner's Name" self ownerPhone := "Owner's Phone" self ownerAddress := "Owner's Address" self ownerMemo := "Owner's Memo" ) forward := method( /* forward */ "I don't know it !" println ) ) Memo ownerName println #==> Uninitialized Memo ownerPhone println #==> Uninitialized Memo ownerAddress println #==> Uninitialized Memo ownerMemo println #==> Uninitialized myMemo := Memo clone myMemo ownerName println #==> Owner's Name myMemo ownerPhone println #==> Owner's Phone myMemo ownerAddress println #==> Owner's Address myMemo ownerMemo println #==> Owner's Memo myMemo hogehoge #==> I don't know it ! myMemo slotNames println #==> list(ownerMemo, ownerName, ownerPhone, ownerAddress) Memo slotNames println #==> list(init, type, ownerPhone, ownerAddress, ownerMemo, ownerName) slotNames println #==> list(Lobby, Protos, set_, exit, Memo, myMemo, _, forward) slotSummary println #==> Object_0x4675c0: #==> Lobby = Object_0x4675c0 #==> Memo = Memo_0x215b450 #==> Protos = Object_0x467560 #==> _ = nil #==> exit = method(...) #==> forward = method(...) #==> myMemo = Memo_0x215bae0 #==> set_ = methjod(...)
また、上では slotNames と slotSummary についてもテストしているのでその結果を確認してほしい。
次はマップについてだ。
Io言語のマップは特定のリテラル形式がないようなのだ。atPut によって追加していくか with を用いる。あるいは、list のリテラル形式を使って作成して、あとでマップとして利用する方法もあると思うが、そのときの効率の問題については分からない。また、マップの内容を表示する場合は asList を用いてリストデータとして表示する。(右端が切れてしまったがスクロールが可能)
// // Map // m := Map clone // Mapオブジェクトの生成 m atPut("aaa", "AAA") // atPutでMapデータを追加していく m atPut("bbb", "BBB") m atPut("ccc", "CCC") m println #==> Map_0x204ac70: m type println #==> Map m asList println #==> list(list(ccc, CCC), list(aaa, AAA), list(bbb, BBB)) m keys println #==> list(ccc, bbb, aaa) m values println #==> list(CCC, BBB, AAA) m size println #==> 3 m hasKey("aaa") println #==> true m hasKey("ddd") println #==> false m hasValue("AAA") println #==> true m hasValue("DDD") println #==> false m at("aaa") println #==> AAA m at("bbb") println #==> BBB m at("ccc") println #==> CCC n := Map clone // Mapオブジェクトの生成 n atPut("ddd", "DDD") m = m merge(n) // Map m に Map nをマージ m asList println #==> list(list(ccc, CCC), list(aaa, AAA), list(ddd, DDD), list(bbb, BBB)) m removeAt("ddd") // Map m の項目の削除 m asList println #==> list(list(ccc, CCC), list(aaa, AAA), list(bbb, BBB)) m empty // Map m のデータを全削除 m asList println #==> list() m println #==> Map_0x1fdf260 空のMap m = m with( /* withでMapデータを作成 */ "xxx", "XXX", "yyy", "YYY", "zzz", "ZZZ" ) m asList println #==> list(list(zzz, ZZZ), list(yyy, YYY), list(xxx, XXX)) l := List clone // Listオブジェクトの生成 l = list( /* Map候補をListリテラルで作成 */ list("aaa", "AAA"), list("bbb", "BBB"), list("ccc", "CCC") ) l println #==> list(list(aaa, AAA), list(bbb, BBB), list(ccc, CCC)) l type println #==> List lMap := l asMap lMap println #==> Map_0x204d730: lMap asList println #==> list(list(ccc, CCC), list(aaa, AAA), list(bbb, BBB))
今回は以上とする。
Io について書けるまたの機会があれば幸いだ。