エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

Ioが面白い・その0 の その6 そろそろシュート練習

「3日目」。くどいようですが「3日目」というのは、Ioのエントリを書き始めて3日目という意味でなくて、本の章の名前です。

7つの言語 7つの世界

7つの言語 7つの世界

今回のゴール

今回は次のスクリプトを解釈できるようにするところまで進みます。

m = { "foo" => 1, "bar" => 2, "baz" => 3 }
print(m["foo"], "\n")

なんかRubyスクリプトのようにしか見えませんが、実際、Rubyで動作します。

$ irb
irb(main):001:0> m = { "foo" => 1, "bar" => 2, "baz" => 3 }
=> {"baz"=>3, "bar"=>2, "foo"=>1}
irb(main):002:0> print(m["foo"], "\n")
1
=> nil

まずは本の例にのっとって話を進めることに

次のような「電話帳」を読み込む例が62ページで出てきます。

{
    "Bob Smith": "5195551212",
    "Mary Walsh": "4162223434"
}

1回目に読んだときの感想。「読み込むって、どういうこと?」

ようやく理解したのは。上のように書いたとき、次のように書いたときと同じ動作をさせるということでした。

Map clone atPut("Bob Smith", "5195551212") atPut("Mary Walsh", "412223434")


分解して理解していくことにします。

代入演算子は2引数メソッド

まず最初にすることは、コロン(:)を演算子として定義して"a":"b"と書いたらatPut("a", "b")と機能するようにすること。

代入演算子を定義するのはOperatorTableaddAssignOperatorを使います。

Io> OperatorTable addAssignOperator(":", "atPut")
(中略)
Assign Operators
  :   atPut
  ::= newSlot
  :=  setSlot
  =   updateSlot

これで"a":"b"と書いたらatPut("a", "b")と機能するはずで、Mapのクローンmに対してm "a":"b"と書いたらm atPut("a", "b")と解釈されるはずです。


m "a":"b"という書き方が少々妙な感じもしますが、ほかの代入演算子m a := bという書き方をしているわけで。等号が含まれていないので妙に感じるのかもしれません。
ちなみに、既存の代入演算子はレシーバを指定せずa := 1と書いても機能しますが、これはレシーバを省略した場合はマスター名前空間のオブジェクトLobbyへメッセージを送ったと解釈されるためです。ですのでasPutを持たないオブジェクトに対して"a":"b"と書いてもエラーになります。


さて。実験。

Io> m := Map clone
==>  Map_0x5240e0:

Io> m "a":"b"
==>  Map_0x5240e0:

Io> m at("a")
==> nil

第一段階混乱。"b"はどこへ消えた?


Ioのプログラミングガイドを見ると次のように書かれています。

These operators are compiled to normal messages whose methods can be overridden. For example:

source compiles to
a ::= 1 newSlot("a", 1)
a := 1 setSlot("a", 1)
a = 1 updateSlot("a", 1)


Io Programming Guide / Assignment


第1引数は自動的に文字列として解釈されるようです。つまり"a":"b"と書いたことでatPut("\"a\"", "b")と解釈されてしまったようです。

実際にそうなっているかためしてみると。

Io> m at("\"a\"")
==> b

そうなってました。


この解説も本の中に書かれてはいるのですが…。

第1引数は名前(つまり文字列)、第2引数は値として解釈される。したがって key : value は atPutNumber("key", value) のように解釈される。次に進もう。


7つの言語 7つの世界 p.63

さらっと「次に進もう」って書かれていたものだから… orz 。

ま、それはそれとして。気を取り直して。


本の中ではatPutNumberというメソッドを定義して演算子:に割り当てています。"a":"b"と書いたときにatPutNumber("\"a\"", "b")となりますがatPutNumberの中で余分な引用符を削ってatPut("a", "b")を実行するようにしています。

Io> OperatorTable addAssignOperator(":", "atPutNumber")
(中略)
Assign Operators
  :   atPutNumber
  ::= newSlot
  :=  setSlot
  =   updateSlot
Map atPutNumber := method(self atPut(call evalArgAt(0) asMutable removePrefix("\"") removeSuffix("\""), call evalArgAt(1)))

ここでは第1引数からasMutableを使って変更可能な文字列を作りremovePrefixで前の引用符をremoveSuffixで後ろの引用符を削除しています。

なんかまどろこしいのでbetweenSeqを使ってみます。これは第1引数と第2引数のあいだにある文字列を返すメソッドです。

Map atPutNumber := method(self atPut(call evalArgAt(0) betweenSeq("\"", "\""), call evalArgAt(1)))


実行結果。

Io> m "a":"b"
==>  Map_0x5240e0:

Io> m at("a")
==> b

期待どおりの結果になりました。

謎のcurlyBrackets

次に括弧の中の解釈です。

本には次のように書かれています。

パーサーは、中括弧({ })に出会うたびに curlyBrackets メソッドを呼び出す。


7つの言語 7つの世界 p.63


…。curlyBrackets めそっどッテ、ナンデスカ?
第二段階混乱。


curly brackets{ }のことだというのは知っていますが、curlyBracketsメソッドについてこれ以上の言及がありません。公式サイトを探しても、見つからない、見つからない。「パーサーは、中括弧に出会うたびに curlyBrackets メソッドを呼び出す」という言葉から察するに{a, b, c}と書いたらcurlyBrackets(a, b, c)となるのだろうと予想されるのですが。


ここで都合のよいことに、メッセージを解釈するmessageメソッドというものがあります。先ほど新しく定義した代入演算子を解釈させてみると、

Io> message("a":"b")
==> atPutNumber(""a"", "b")

atPutNumberメソッドになることがわかります。


同様にやってみると。

Io> message({a,b,c})
==> curlyBrackets(a, b, c)

と、予想どおりの結果となることがわかりました。


Mapを生成するメソッドを設定します。

curlyBrackets := method(
  r := Map clone
  call message arguments foreach(arg, r doMessage(arg))
  r
)


これで準備が整いました。ためしにmessageメッセージで解釈させてみます。

Io> message({ "foo":1, "bar":2, "baz":3 })
==> curlyBrackets(atPutNumber(""foo"", 1), atPutNumber(""bar"", 2), atPutNumber(""baz"", 3))

定義したメソッドに展開されているようすがわかります。


実際にMapのクローンを生成し、値を取り出すところまでやってみます。

Io> m := { "foo":1, "bar":2, "baz":3 }
==>  Map_0x522ec0:

Io> m at("foo")
==> 1

curly brackets があるなら square brackets もあるのだろう

ありました。

Io> message([a, b, c])
==> squareBrackets(a, b, c)


というわけで、Mapから値を取り出すatの機能をsquareBracketsに定義してやります。

Map squareBrackets := Map getSlot("at")

これでm["foo"]と書いたらm at("foo")と同じ動作をするようになりました。

Io> m["foo"]
==> 1

ゴールまであと少し

今回は次のスクリプトを解釈できるようにするところまで進むことをゴールとしました。くり返しになりますがこのスクリプトRubyで動作します。

m = { "foo" => 1, "bar" => 2, "baz" => 3 }
print(m["foo"], "\n")


あとたりないのは=>演算子printメソッド。


=>演算子:演算子を定義したときと同じ要領で定義できます。

OperatorTable addAssignOperator("=>", "atPutNumber")

またIoにはRubyprintと似た動作をするwriteメソッドがあります。

Io> write(m["foo"], "\n")
1
==> nil

Ioにもprintメソッドがあるのですが、マスター名前空間で使った場合Lobbyの内容を表示するだけのものなので、これをwriteで書き換えることにします。

print := getSlot("write")


あともうひとつ。Ioで=演算子は、定義ずみのスロットを更新する演算子なので、使う前に:=演算子::=演算子でスロットが定義されていなければなりません。

ちょっと卑怯っぽいんですが、スロットを定義しておきます。

m := nil


件のスクリプトをRubyHash.rbという名前で保存。

m = { "foo" => 1, "bar" => 2, "baz" => 3 }
print(m["foo"], "\n")


定義したIoのスクリプトをまとめてPseudoRubyHash.ioという名前で保存。

OperatorTable addAssignOperator("=>", "atPutNumber")

curlyBrackets := method(
  r := Map clone
  call message arguments foreach(arg, r doMessage(arg))
  r
)

Map atPutNumber := method(self atPut(call evalArgAt(0) betweenSeq("\"", "\""), call evalArgAt(1)))

Map squareBrackets := Map getSlot("at")

print := getSlot("write")

m := nil

doFile(System args at(1))


Rubyでの実行結果。

$ ruby RubyHash.rb 
1


Ioでの実行結果。

$ io PseudoRubyHash.io RubyHash.rb 
1


ゴラッソ。

でも解決できていないことが

doFile(System args at(1))というのは、コマンドラインの第1パラメータが示すファイルをIoスクリプトとして評価する、ということなんですが。
なんでこの1行の代わりにRubyHash.rbに書いたスクリプトを書かなかったかというと…。


PseudoRubyHash.ioのdoFileの行をRubyHash.rbの内容で書き換えたPseudoRubyHash2.ioを実行してみると。

$ io PseudoRubyHash2.io 

  Exception: Sequence does not respond to '=>'
  ---------
  Sequence =>                          PseudoRubyHash2.io 17
  Object curlyBrackets                 PseudoRubyHash2.io 17
  CLI doFile                           Z_CLI.io 140
  CLI run                              IoState_runCLI() 1


なぜか=>演算子が評価されません。OperatorTableに登録されているのは確かめたのですが、それだけではたりないみたいです。
第三段階混乱。


どうもコンテクストが絡んでいるようで、現在のコンテクスト上で実行するdoFileを介してならきちんと評価されてます。
Ioの中身ものぞき込んでいるのですが、いまのところ未解決。



なんとなくもやもやしたものを残しつつ。次回につづく。