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

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

Ioが面白い・その0 の その5 そろそろピッチにでてウォーミングアップ

「2日目」のつづきのつづき。念のためもう一度言うと、「2日目」というのは、Ioのエントリを書き始めて2日目という意味でなくて、本の章の名前です。今日は「2日目」の3日目です。

7つの言語 7つの世界

7つの言語 7つの世界

メッセージ

Ioはオブジェクト指向なので、「オブジェクトにメッセージを送る」というのがプログラムを駆動する基本になります。

メッセージの内容を知りたいばあい、メソッド内でcallメソッドが使えます。

Io> Object myMethod := method(call println)
==> method(
    call println
)
Io> Object myMethod
 Call_0x4af930:

==>  Call_0x4af930:

callメソッド」というと、なにかを呼ぶ(コールする)という印象がありますが、実体はCallオブジェクトをプロトタイプに持つオブジェクトの値です。なので、なにかを呼ぶというよりも「呼び出しという事象」をあらわすものととらえた方がよさそうです。動詞のcallでなくて名詞のcallですね。


Callオブジェクトには呼び出し元を示すsender、呼び出し先を示すtarget、メッセージを示すmessageというメソッドがあります。
実際の様子を見てみます。結果を見やすくするためasStringを書き換えてオブジェクトの名前を表示するように細工しています。

Io> Lobby asString := "LOBBY"
==> LOBBY
Io> foo := Object clone
==>  Object_0x3a11b0:

Io> foo asString := "FOO"
==> FOO
Io> foo myMethod := method(writeln("target:", call target, " / sender:", call sender, " / message:", call message))
==> method(
    writeln("target:", call target, " / sender:", call sender, " / message:", call message)
)
Io> foo myMethod
target:FOO / sender:LOBBY / message:myMethod
==> nil

マスター名前空間LobbyからfooへメッセージmyMethodを送っている様子がわかります。

またcall messageで得られるオブジェクトはMessageオブジェクトのクローンで、argAtメソッドで引数を取り出すことができます。

Io> foo myMethod2 := method(writeln("first arg:", call message argAt(0), " / second arg:", call message argAt(1), " / third arg:", call message argAt(2)))
==> method(
    writeln("first arg:", call message argAt(0), " / second arg:", call message argAt(1), " / third arg:", call message argAt(2))
)
Io> foo myMethod2(1,2,3)
first arg:1 / second arg:2 / third arg:3
==> nil
Io> foo myMethod2(1,2)
first arg:1 / second arg:2 / third arg:nil
==> nil

下の例は第3引数を指定しなかったためargAt(2)の値がnilになっています。


ところで。
メソッドの定義をするとき、仮引数を指定することができました。

Io> Object showA := method(s, s println)
==> method(s, 
    s println
)

ここでsが仮引数。

一方で、上記のようにcall message argAt(n)でも引数を得ることができます。

Io> Object showB := method(call message argAt(0) println)
==> method(
    call message argAt(0) println
)

このAとBで何が違うのか。試してみます。

Io> Object showA(1)
1
==> 1
Io> Object showB(1)
1
==> 1
Io> Object showA("hoge")
hoge
==> hoge
Io> Object showB("hoge")
"hoge"
==> "hoge"
Io> Object showA(1 + 2)
3
==> 3
Io> Object showB(1 + 2)
1 +(2)
==> 1 +(2)

Bのばあい、メッセージが評価されずそのまま渡されているのがわかります。条件分岐のところで書いた遅延評価はこのようなしくみで実現されているようです。

とはいえ。どこかでメッセージを評価しないと値を得られません。メッセージを評価するにはdoMessageメソッドを使います。
Bのメソッドを次のように書き換えます。

Io> Object showB := method(doMessage(call message argAt(0)) println)
==> method(
    doMessage(call message argAt(0)) println
)

これでさきほどの例を試してみます。

Io> Object showB(1 + 2)
3
==> 3

メッセージが評価されたのがわかります。


次に。foomyMethodから、foomyNameを引数に与えてA、Bのそれぞれのメソッドを実行してみます。

Io> foo myName := "FOO"
==> FOO
Io> foo myMethod := method(Object showA(myName); Object showB(myName))
==> method(
    Object showA(myName); Object showB(myName)
)

実行結果。

Io> foo myMethod
FOO

  Exception: Object does not respond to 'myName'
  ---------
  Object myName                        Command Line 1
  Object showB                         Command Line 1
  Object myMethod                      Command Line 1

エラーメッセージは「ObjectにはmyNameに対応できない」。つまりObjectmyNameを持っていないと主張しています。


何が起こったのか。


上で説明したようにcall message argAt(0)は評価前のメッセージそのもを示しています。今回のばあいmyNameです。そのためdoMessage(call message argAt(0)) printlnの意味はdoMessage(myName) printlnとなります。


このときdoMessage(myMessage)Objectで実行されているため、myNameObjectへのメッセージとして評価されてしまいます。しかしObjectmyNameを持っていないためエラーとなった、というのが事の真相です。


メッセージが未評価のままそのまま渡されるので、評価したときの文脈(context)によって評価結果が異なる、という動作をIoではするようです。


一見、面倒で危なっかしく見えますが(実際いろいろ危険だとは思うけど)、メソッドが文脈を知っているためメッセージの評価(いつ評価するのか、何に対して評価するのか)を柔軟に操作することができるようになっています。


それはそれとして。今回のような場合はどうしたらよいのか。


myNameObjectへのメッセージでなく、送信元のfooへのメッセージと解釈させればよいので、doMessageを送信元、つまりcall senderに適用することで期待する結果を得る事ができるようになります。

showBをさらに書き換え。

Io> Object showB := method(call sender doMessage(call message argAt(0)) println) 
==> method(
    call sender doMessage(call message argAt(0)) println
)

これでfoo myMethodを実行してみると。

Io> foo myMethod
FOO
FOO
==> FOO

このようにA、Bとも同じ結果になりました。

ちなみに。逆の視点で見ると。Aのように書いたばあい(仮引数を書いたばあい)は、メソッドの呼び出しの前に引数が評価される、ということになりそうです。


これらをふまえて。
本の例にもあったunlessifと逆の分岐)を実装。

Io> unless := method((call sender doMessage(call message argAt(0))) ifFalse(call sender doMessage(call message argAt(1))) ifTrue(call sender doMessage(call message argAt(2))))
==> method(
    (call sender doMessage(call message argAt(0))) ifFalse(call sender doMessage(call message argAt(1))) ifTrue(call sender doMessage(call message argAt(2)))
)
Io> unless(true, "FALSE" println, "TRUE" println)
TRUE
==> true
Io> unless(false, "FALSE" println, "TRUE" println)
FALSE
==> false


はい。とても面倒です。
Ioの作者も面倒と思ったようで、頻繁にでてくるであろうcall sender doMessage(call message argAt(n))は、call evalArgAt(n)と短く書けるようになっています。

Io> unless := method( (call evalArgAt(0)) ifFalse(call evalArgAt(1)) ifTrue(call evalArgAt(2)))
==> method(
    (call evalArgAt(0)) ifFalse(call evalArgAt(1)) ifTrue(call evalArgAt(2))
)
Io> unless(true, "FALSE" println, "TRUE" println)
TRUE
==> true
Io> unless(false, "FALSE" println, "TRUE" println)
FALSE
==> false


このようなショートカットはいくつか用意されています。

元の形 ショートカット
call message argAt(n) call argAt(n)
call message argCount call argCount
call sender doMessage(call message argAt(n)) call evalArgAt(n)

この他に、すべての引数を送信元の文脈で評価した結果をリストで返すcall evalArgsというものもあります。



と、いうところで。ようやく「2日目」終了