uehaj's blog

Grな日々 - GroovyとかGrailsとかElmとかRustとかHaskellとかReactとかFregeとかJavaとか -

インスタンスごとのメタクラスを使っての「電卓を作ろう」2番煎じバージョン

id:fumokmmさんが書かれた「電卓を作ろう」のお題プログラムを、私の好きな「Javaにも適用可能なインスタンスごとのメタクラス」機能を使って、失礼をばして、ちょっとだけ書き直してみました。

文字列オブジェクトインスタンスにmetaClassを通じてpush()アクションを追加するというわけです。switch-case文はすっかりなくなります。

前も書きましたが、インスタンスごとのメタクラス(特にJavaクラス、特にStringクラスに対するもの)は、クラス指向ではない「プロトタイプベースのオブジェクト指向」を実現するものであり、一見地味に見えますが、Groovy 1.6の拡張機能の中で、結構大きいインパクトを持った、プログラミングスタイルを変えてしまう可能性のある*1、重要な機能拡張だと思います。またこの方向性は簡潔さを求めるGroovyの理念にも合致しているので,どんどん使っていいと私は思います。簡単ですしね。

ちなみにStringがなぜ重要かというと、Java APIにString#intern()があって利便が良いからです。文字列リテラルとinternに関するJVM仕様をちゃんと理解してないと、インスタンスの同値性に関してははまるかもしれません。

import groovy.swing.*
import javax.swing.*

//---------------------------------------GUI作成 ここから

def lb

new SwingBuilder().frame(title: '簡易電卓', pack: true, show: true,
	defaultCloseOperation : JFrame.EXIT_ON_CLOSE) {
  panel {
    vbox {
      /* 結果出力欄 */
      hbox {
        lb = label('0')
      }
      /* 各種ボタン */
      hbox {
        ['7','8','9','+'].each { lab ->
          button(lab, actionPerformed: { lab.push() } )
        }
      }
      hbox {
        ['4','5','6','-'].each { lab ->
          button(lab, actionPerformed: { lab.push() } )
        }
      }
      hbox {
        ['1','2','3','*'].each { lab ->
          button(lab, actionPerformed: { lab.push() } )
        }
      }
      hbox {
        ['0','=','C','/'].each { lab ->
          button(lab, actionPerformed: { lab.push() } )
        }
      }
    }
  }
}.visible = true
//---------------------------------------GUI作成 ここまで

def stock = null	// '数値+オペレータ'の計算途中をカリー化したクロージャとして保持
def before = null	// 一個前にPUSHされたものを保持しておく

('0'..'9').each { digit ->
  digit.intern().metaClass.push = {
    if (!before || before in '0'..'9') {
      lb.text = lb.text + digit
      lb.text = (lb.text as int) as String
      /* 一個前にオペレータが押された場合 */
    } else if (before in ['+', '-', '*', '/', '=']) {
      lb.text = digit
    }
    /* 一個前にPUSHされたものとして保持しておく */
    before = digit
  }
}

['+':'plus', '-':'minus', '*':'multiply', '/':'div'].each { k, v ->
  k.metaClass.push = {
    /* 一個前に数値が押された場合 */
    if (before in '0'..'9') {
      /* ストックされている場合は、計算する */
      if (stock) {
        /* カリー化された計算途中クロージャを完結させ結果を得る */
        lb.text = stock.call(lb.text as int)  as String
        stock = null
      } else {
        /* ストックされていない場合は、指定された演算を実行するクロージャ
         * の第一引数をカリー化した結果としてストック
         */
        stock = { lhs, rhs -> lhs."$v"(rhs) as int}.curry(lb.text as int)
      }
      /* 一個前にPUSHされたものとして保持しておく */
      before = k
    }
  }
}

'='.metaClass.push = {
  if (before in '0'..'9') {
    /* ストックされている場合は、計算する */
    if (stock) {
      lb.text = stock.call(lb.text as int)
      stock = null
    }
    /* 一個前にPUSHされたものとして現在の結果に表示されている数値の末尾を保持しておく */
    before = lb.text[-1]
  }
}

'C'.metaClass.push = {
  lb.text = '0'
  stack = null
  before = null
}


*1:言い過ぎかも。