Line 1: Error: Invalid Blog('by Esehara' )

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

>> Zanmemo

あと何かあれは 「esehara あっと じーめーる」 か @esehara まで

Rubyでifとwhileをメソッド化する -- Smalltalkライクな構文をRubyにも

今日の料理

f:id:nisemono_san:20160612213909j:plain

私は「ためしてガッテン」の信徒です。

はじめに

いまYAP(achimon)Cレジュメを書いているのだけれども、いろんなプログラミング言語を触ってみての感想が多く入ってきていて、乱雑な感じになっている。

いろいろなことを乱雑に詰めこむのは、自分の悪いところでもあり、良いところでもあると思って開きなおっているが、それとは関係なく、Smalltalkについて、「これってちょっと頑張ればRubyにも実装できることじゃないかな」ということに気がついたので、どれだけの需要があるのかわからないけれども、記事にしておく。

Smalltalkにおけるif

Smalltalk(以下、Pharoの処理系を前提とする)を触ってみて面白いなあ、と思ったことの一つに、条件分岐もメソッドで表現するということだった。過去にも書いた通り、FizzBuzzは次のようにして書ける:

((1 to: 100) collect: 
    [ :i |
    (i % 15 == 0) 
        ifTrue:'FizzBuzz'
        ifFalse:
    [(i % 5 == 0)
        ifTrue: 'Buzz'
        ifFalse:
    [(i % 3 == 0)
        ifTrue: 'Fizz'
        ifFalse: i asString ]]])
    do: [:i | Transcript show: i asString; cr]. 

このカッコは、いわゆるブロックと呼ばれるものだ。で、なんでSmalltalkでifを表現するさいにブロックにするかと言うと、次のコードを実行して見るとわかりやすい:

|i|
(1 = 1) ifTrue: (i := 10) ifFalse: (i := 20).
Transcript show: i.

Smalltalkでは、:=は代入を現すのだれど、このとき、iの部分に何が代入されているかというと、20が代入 されている。

それは当然のことで、ifTrueifFalseはメソッド呼び出し(正確にはメッセージング)なんだから、その値はメソッドに入る前に評価されることになる。なもんだから、iに入る値は20になる。さらにややこしいことに、ifTrue: ifFalse:の値は10を返すようになっている。

SICPなんかを読んだことがある人ならわかるように、ifなどの式の場合、条件となる式がtrueかfalseかどうか判定したあとに、その分岐となる式を評価するという手順を取る(もちろん、式が文の場合もある)。

もうすこし噛み砕いて言うと、真偽値が定まるまで、それ以降の式(文)に関しては、評価を保留して欲しいわけである。

さて、このときに使えるのが、何らかの無名関数にラップして、その式の評価を、外側の関数が呼び出されるまで保留するという方法である。要はその式の評価を何らかの方法で遅らせればよい。そこで、Smalltalkでは[]と呼ばれるブロックを使う。

|i|
(1 = 1) ifTrue: [i := 10] ifFalse: [i := 20].
Transcript show: i; cr.

これで意図した10iに入ってくる。つまり、ifFalseにおける評価はその場では行なわれなかったということになる。ちなみに、このブロックはvalueというメッセージを送ることによって、評価することが可能になる。

|i|
(1 = 1) ifTrue: ([i := 10] value) ifFalse: ([i := 20] value).
Transcript show: i; cr.

これでさっきみたいに、i20が入るようになる。

Rubyで似たようなものを実装する

Rubyにもブロックみたいなものが存在する。それがProcだ。

def echo
  yield
end
echo {puts "Welcome, Block."}

といったように、カッコで渡されたProcyieldで実行するといったような使い方ができる。で、このProcという奴は、結局はクラスで、Rubyはクラスの定義をある程度いじれるようになっている。

RubyはBooleanクラスみたいなものは存在していないようで、基本的にはTrueClassFalseClassに分かれているだけという作りになっているようだ。で、今回の目的としては、このクラスに対してメソッドを生やして、 ifTrueifFalseというメソッドを作って、Smalltalk風にしてしまおうというのが今回のエントリとなる。

といっても、そんな大層なことはない。例えば、Pharoのインスペクタで、ifTrue: ifFalse:のコードを見てみればわかる通り(次の例はFalseクラスの場合である):

ifTrue: trueAlternativeBlock ifFalse: falseAlternativeBlock 
    ^falseAlternativeBlock value

単純に二つのブロックを受けとって、片方のブロックを採用しているだけである。さらには、このFalseにはifTrue:という単体のメソッドも存在しているのだけれども、これも思いきっていて:

ifTrue: alternativeBlock 
    ^nil

ただ、nilを返すだけのメソッドであることがわかる。これらを参考に、Rubyでも同じメソッドを作るとすると次のようになる:

class TrueClass
  def ifTrue
    yield
  end
  
  def ifFalse
    self
  end
end

class FalseClass
  def ifTrue
    self
  end
  
  def ifFalse
    yield
  end
end

class Object
  def ifTrue
    self
  end
  
  def ifFalse
    self
  end
end

元のSmalltalkとは違うのは、まずメソッドチェインで実装しているために、ifTrue:という単体の場合と、ifTrue: ifFalse:という場合に分けることができないので、とりあえず自分自身を返すことで、それらしい振るまいをさせている。

あともう一つ困るのは、nilが入ってきた場合で、NilClassのときにそこでメソッドチェインがとぎれてしまうことだ。なので、nilやそれ以外の値が入ってきた場合に、それ自身を返してメソッドをとぎれさせないようにする必要がある。基本的に、採用された値がそのまま返ってきているだけだろうし、多分これをRubyで実用的に使うことが無いという、最悪な妥協によって、このような実装となっている。こうすることによって:

1.upto(10).each do |n|
  (n % 15 == 0)
  .ifTrue  { puts "FizzBuzz" }
  .ifFalse { 
    (n % 3 == 0 )
    .ifTrue { puts "Fizz"}
    .ifFalse {
      (n % 5 == 0)
      .ifTrue  { puts "Buzz" }
      .ifFalse { puts n}
      }
    }
end

SmalltalkみたいなFizzBuzzを再現することができる。

ついでだし、whileも実装しよう

また、SmalltalkのブロックにはwhileTrue:というメソッドが用意されている;

|i|
i := 0.
[ i < 10 ] whileTrue:
    [Transcript show: i; cr. i := i + 1. ].

これは、PharoにおいてはBlockClosureというクラスとなっている。このwhileTrue:もメソッドの中身を見れるので確認してみると:

whileTrue 
    self value ifTrue: [ self whileTrue ]

Smalltalkの場合、左結合だけを意識していればよいので、そのように読むと、まず自分自身のブロックを評価する。このとき、TrueFalseというクラスになる。Trueであるならば、ifTrueはブロックを採用する。そして、自らのwhileTrueを再帰的に呼び出す、というわけだ(もちろん、これが一回ブロックに入っている理由は、ifTrueに入る前に評価されてしまうと無限ループに陥いってしまうからだ)。

Pharoのインスペクトを見ると、whileFalse, whileNil, whileNotNilなど豪華なのだが(とはいえ、Rubyにもuntilはある)、whileといえば「評価した式がTrueの場合には、そのブロックを実行する」ということであるので、それに従って実装をしてみよう。

先に、なぜBlockClosureに対してwhileというメソッドを生やしているのか、というのは、例えばTrueClasswhileというメソッドを生やしたとして、そのインスタンスはTrueであることは既に決定済みなわけだから、結局のところ無限ループに陥いる。whileにおいて重要なのは、iという変数を評価式で判定していった際に、いつかFalseになる可能性があるということだ。だから、何度も式を評価しなければならない。

……と、実装の方向が掴めたところで、RubyのProcに対しても、同じメソッドを定義してみよう。

class Proc
  def while(&block)
    self[].ifTrue do
      block.call
      self.while &block
    end
  end

  def until(&block)
    self[].ifFalse do
      block.call
      self.until &block
    end
  end
end

このように定義することによって、次のようにwhileuntilをメソッドのように使うことができる。

i = 0
->{ i < 10 }.while { puts i ; i += 1 }
->{ i < 1  }.until { puts i ; i -= 1 }

まとめ

一見して「そのプログラミング言語の特殊な実装」のように見えることでも、ちゃんとその理由と背景を洞察してみることによって、他のプログラミング言語でも同じような実装が可能だったりする。今回の方法が直接的に役に立つかどうかはわからないところがあるけども、また一つ新しい知見が得られたことは嬉しかったし、単純に楽しかった。

大切な宣伝

ちなみにどれだけ需要があるかわかりませんが、YAP(achimon)Cではこういうトークを延々と25分ほどやるつもりなので、是非聞きに来てね!! いや、俺のトークはどうだっていいんだ、もっと楽しいトークが沢山あるはずのYAP(achimon)Cは7月2日から7月3日にかけてあるから、みんな来るんだ!!