倭マン's BLOG

くだらない日々の日記書いてます。 たまにプログラミング関連の記事書いてます。 書いてます。

Closure クラスの API を使ってみる (1) : パラメータ情報 getMaximumNumberOfParameters(), getParameterTypes()

前の記事で Closure オブジェクトを 実行するメソッド call(), run() を見ましたが、それの続編として Closure クラスに定義されている他のメソッドを使ってみます。 今回はパラメータ(引数)に関連するメソッドです。

目次

  1. パラメータ情報 getMaximumNumberOfParameters(), getParameterTypes()
  2. this, owner, and delegate
  3. 制御構造!? isCase(), directive
  4. クロージャの合成 leftShift(), rightShift()
  5. カリー化(部分適用?) curry(), rcurry(), ncurry()
  6. トランポリン trampoline()
  7. メモ化 memoize()

パラメータに関するメソッド

Closure のパラメータ情報を取得するメソッドには

    int getMaximumNumberOfParameters()
    Class[] getParameterTypes()

があります。 パラメータにデフォルト値が設定されてるかどうかを取得するメソッドは見当たりませんな。 まぁそういうもんだんでしょう。 それはそれとして、上記のメソッドをそれぞれ見ていきましょう。

getMaximumNumberOfParameters() メソッド
このメソッドは、その名の通りパラメータの取り得る最大個数を返します。 単なる個数ではなく最大個数なのは、デフォルト値が設定されていている場合には引数の個数が少なくて済むからでしょう。 具体的なクロージャオブジェクトで挙動を確かめてみると

def c = { println 'no arg' }
assert 1 == c.getMaximumNumberOfParameters()

def c0 = { -> println '0 arg' }
assert 0 == c0.getMaximumNumberOfParameters()

def c1 = { arg -> println '1 arg' }
assert 1 == c1.getMaximumNumberOfParameters()

def c2 = { arg0, arg1 -> println '2 args'}
assert 2 == c2.getMaximumNumberOfParameters()

def c1_2 = { arg0, arg1 = null -> println '1 or 2 args' }
assert 2 == c1_2.getMaximumNumberOfParameters()

となります。 明示的に引数を指定しない場合は1個、デフォルト値が設定されていても個数として勘定される、といったところが注意点でしょう。 とは言っても、メソッド名から挙動は明らかですね。

getParameterTypes() メソッド
getParameterTypes() メソッドは、クロージャのパラメータ(引数)の型を配列として返します。 こちらの挙動も想像がつくと思います。

def c1_2 = { String arg0, arg1 = null -> println '1 or 2 args' }
assert [String.class, Object.class] == (c1_2.getParameterTypes() as List)

型宣言をしないと java.lang.Object 型とみなされます。

サンプル・コード

上記2つのメソッドを使って、ちょっとサンプルコードを書いてみましょう。 実装するのは Closure オブジェクトを引数にとるメソッドです。 コードは Groovy ですが、ところどころあえて Java 風に書いてます。 Closure オブジェクトを引数にとる Java メソッド実装も書けるよ〜ってだけです(どの程度ニーズがあるかは不明ですが:-))。

準備
まずは準備としてベースとなるコードを。 ターゲットにしているのは XML の簡単なオブジェクト・モデルです。 クラスとして定義するのは、要素に対応する Element クラスと属性に対応する Attribute クラスです。 QName は Groovy に内蔵されている groovy.xml.QName を拝借:

import groovy.xml.QName

class Element{

    QName qname
    private final Map<QName, Attribute> atts = [:]
    
    Element(QName qname){ this.qname = qname }
    
    void addAttribute(QName qname, String value){
        def att = new Attribute(qname:qname, value:value)
        atts.put(att.qname, att)
    }
}

class Attribute{
    QName qname
    String value
}

この時点では、要素に属性を加える addAttribute() メソッドは定義されてますが、属性を取得するメソッドは定義されていません。 これからこれを実装していきたいんですが、ユースケースとしては「属性を走査して処理を行う」というものに限ることにします。

Java の場合 attributes() メソッド
まずは Java で実装する場合。 Java5 から導入された拡張 for 文を使って走査するのが簡単なので、Iterable オブジェクトを返すようにしましょう:

import groovy.xml.QName

class Element{
    ...
    
    Iterable<Attribute> attributes(){
        return this.atts.values()
    }
}

def e = new Element(new QName('e', 'my-ns'))
e.addAttribute(new QName('my-ns', 'att1'), 'value1')
e.addAttribute(new QName('my-ns', 'att2'), 'value2')

// Iterable
for(Attribute att : e.attributes()){
    println "$att.qname = $att.value"
}

実行結果は以下のようになります(以下のサンプル・コードでも実行結果は同じです):

{my-ns}att1 = value1
{my-ns}att2 = value2

Iterable オブジェクトを返すようにしておくと拡張 for 文でサクッと走査できるので、これはこれで便利。

Closure を引数にとるメソッド eachAttribute()
では、ここからが本題。 クロージャを引数にとって、各 Attribute オブジェクトを引数にクロージャを実行するようなメソッドを書きます。 こういうのは GDK で Java オブジェクトに追加された each() メソッド(特にコレクション・フレームワークのクラスに対して)と同種のメソッドなので、名前を eachAttribute() にしておきましょう。 実装は簡単です:

class Element{
    ...
    
    void eachAttribute(Closure c){
        for(Attribute att : this.atts.values()) c.call(att)
        // Groovy なら
        //     this.atts.values().each(c)
        // でも OK (for もいらない)
    }
}

def e = new Element(new QName('e', 'my-ns'))
e.addAttribute(new QName('my-ns', 'att1'), 'value1')
e.addAttribute(new QName('my-ns', 'att2'), 'value2')

// eachAttribute() メソッドを使う
e.eachAttribute{ att -> println "$att.qname = $att.value" }

思いの外、簡単に実装できてしまいますね。 for で属性を走査しつつ、各 Attribute オブジェクトを引数にして Closure#call() メソッドでクロージャを実行しているだけです。 Groovy で書くなら、each() メソッドを使って for の走査すら書く必要がありません。 eachAttribute() メソッドの使い方も簡単です。

Closure の引数の個数によって渡すオブジェクトを変える eachAttribute()
さて、ここからが本題の中の本題。 メソッドに渡された Closure オブジェクトに対して、パラメータ情報から call() メソッドに渡す引数を変えてやります。 これによってメソッド実装は少々長くなりますが、使う側は手軽になる場合があります。

上記の eachAttribute() メソッドを書き換えていきますが、挙動は以下のようにします:

  • Closure オブジェクトが引数を1つとるなら Attribute オブジェクトを渡す
  • Closure オブジェクトが引数を2つとるなら QName オブジェクトと属性値のオブジェクトを渡す
  • Closure オブジェクトが引数を3つとるなら QName を表す String オブジェクト、名前空間 URI を表す String オブジェクト、属性値のオブジェクトを渡す
  • それ以外なら MissingMethodException を投げる

実装はこんな感じ:

import groovy.xml.QName

class Element{
    ...
    
    void eachAttribute(Closure c){
        switch(c.getMaximumNumberOfParameters()){
            case 1:
                for(Attribute att : this.atts.values()) c.call(att)
                break
            case 2:
                for(Attribute att : this.atts.values()) c.call(att.qname, att.value)
                break
            case 3:
                for(Attribute att : this.atts.values())
                    c.call(att.qname.getQualifiedName(), att.qname.getNamespaceURI(), att.value)
                break
            default:
                throw new MissingMethodException()
        }
    }
}

def e = new Element(new QName('e', 'my-ns'))
e.addAttribute(new QName('my-ns', 'att1'), 'value1')
e.addAttribute(new QName('my-ns', 'att2'), 'value2')

// each (1 arg)
e.eachAttribute{ att -> println "$att.qname = $att.value" }
println()

// each (2 args)
e.eachAttribute{ qname, value -> println "$qname = $value" }
println()

// each (3 args)
e.eachAttribute{ qname, uri, value -> println "{$uri}$qname = $value" }

3種類の eachAttribute() メソッドの実行がありますが、クロージャの引数に渡されているオブジェクトはそれぞれ異なります。 使う側が使いやすいものを選んで記述できます。 まぁ、あまりに引数が多かったり分岐が多種類あると、メソッドのメンテナンスも面倒だし、使う側もどういった引数が渡されてくるのか把握しづらくなるので凝りすぎないのが無難かと。 また、上記のサンプルでは switch の default で例外を投げてますが、クロージャの引数にデフォルト値が設定されていれば例外なく実行できる場合もあるので、必ずしも例外を投げる必要はないかと思います。

プログラミングGROOVY

プログラミングGROOVY

  • 作者: 関谷和愛,上原潤二,須江信洋,中野靖治
  • 出版社/メーカー: 技術評論社
  • 発売日: 2011/07/06
  • メディア: 単行本(ソフトカバー)
  • 購入: 6人 クリック: 392回
  • この商品を含むブログ (155件) を見る