かとじゅんの技術日誌

技術の話をするところ

ScalaとGoogle Collectionsで関数オブジェクト

あけましておめでとうございます。
年末年始はいかがでしたか?私は大晦日から実家の兵庫に向かう道中、滋賀で大雪に見舞われまして、10時間ぐらい立ち往生して車中泊という幸先悪い年越しでしたorz

さて、今日は関数オブジェクトの話です。関数オブジェクトといえばScalaですね。

たとえば、PersonNameの集合に対して、ある条件に該当するPersonNameのインスタンスだけを取得したい場合に、以下のような関数を使うとさくっと取得できます。filterメソッドのところです。

scala> case class PersonName(firstName:String, lastName:String)
defined class PersonName

scala> import scala.collection.immutable.HashSet
import scala.collection.immutable.HashSet
scala> var personNames:Set[PersonName] = new HashSet[PersonName]
personNames: Set[PersonName] = Set()

scala> personNames += PersonName("Junichi","Kato")
scala> personNames += PersonName("JUNICHI","Kato")
scala> personNames += PersonName("Junichi","KATO")
scala> personNames += PersonName("JUNICHI","KATO")

scala> personNames.filter(p => p.firstName == "Junichi")
res4: scala.collection.immutable.Set[PersonName] = Set(PersonName(Junichi,KATO), PersonName(Junichi,Kato))
// 上記はpersonNames.filter(_.firstName == "Junichi")と書ける

このfilterメソッドに渡しているのが述語(Predicate)です。これも関数オブジェクトで、PersonNameを受け取ってBooleanを返す述語関数です。Scala的には PersonName => Booleanと表します。検索条件が複雑なら述語を使うのも考えてみてもいいかもしれません。

これと同じことJavaでもできます。Google Collectionsを使います。

Set<PersonName> personNames = new HashSet<PersonName>();
		
personNames.add(new PersonName("Junichi","Kato"));
personNames.add(new PersonName("JUNICHI","Kato"));
personNames.add(new PersonName("Junichi","KATO"));
personNames.add(new PersonName("JUNICHI","KATO"));

Predicate<PersonName> predicate1 = new Predicate<PersonName>() {			
  @Override
  public boolean apply(PersonName input) {
    return input.getFirstName().equals("Junichi");
  }			
};
		
Set<PersonName> result = Sets.filter(personNames, predicate1);

assertThat(result, is(notNullValue()));
assertThat(result.size(), is(2));

これぐらいの検索なら、for文をぶんまわしたほうがいいのですがw
関数を組み合わせて使うと、こういうことができます。

Predicate<PersonName> predicate2 = new Predicate<PersonName>() {			
  @Override
  public boolean apply(PersonName input) {
    return input.getLastName().equals("KATO");
  }			
};
// AND条件を作る
Set<PersonName> filter2 = Sets.filter(personNames, Predicates.and(predicate1, predicate2));
		
assertThat(filter2, is(notNullValue()));
assertThat(filter2.size(), is(1));

このANDの正体はAndPredicateという関数オブジェクトで、すべてのPredicateのapplyがtrueを返す場合だけtrueを返します。他にもPredicates#orなどがありますね。

また、Scalaに戻りますが、以下の例ではmapメソッドの引数に、PersonNameの要素を受け取って別のオブジェクトに変換する関数を渡しています。つまり変換器をmapメソッドに渡して、その変換器に各要素を適用しその変換結果をコレクションとして返します。この例では、PersonNameをフルネームの文字列の集合に変換しています。

scala> case class PersonName(firstName:String, lastName:String)
defined class PersonName

scala> import scala.collection.immutable.HashSet
import scala.collection.immutable.HashSet
scala> var personNames:Set[PersonName] = new HashSet[PersonName]
personNames: Set[PersonName] = Set()

scala> personNames += PersonName("Junichi","Kato")
scala> personNames += PersonName("JUNICHI","Kato")
scala> personNames += PersonName("Junichi","KATO")
scala> personNames += PersonName("JUNICHI","KATO")

scala> personNames.map(p => p.firstName + "." + p.lastName)
res4: scala.collection.immutable.Set[java.lang.String] = Set(Junichi.KATO, JUNICHI.KATO, JUNICHI.Kato, Junichi.Kato)

これと同じことはGoogle Collectionsでも可能です。

Collection<PersonName> personNames = new HashSet<PersonName>();
		
personNames.add(new PersonName("Junichi", "Kato"));
personNames.add(new PersonName("JUNICHI", "Kato"));
personNames.add(new PersonName("Junichi", "KATO"));
personNames.add(new PersonName("JUNICHI", "KATO"));
		
Function<PersonName, String> fullNameFunction = new Function<PersonName, String>() {
  @Override
  public String apply(PersonName from) {
    return from.getFirstName() + "." + from.getLastName();
  }
};
// フルネームに変換する		
Collection<String> transform = Collections2.transform(personNames, fullNameFunction);

for (String fullName : transform) {
  System.out.println(fullName);
}

さらに複数の関数オブジェクトを合成させることができます。
下記のFunction composeは以下の二つの関数を合成した関数になっています。つまり、PersonNameもfirstNameとlastNameを入れ替えて、それをフルネームの文字列に変換する関数です。

y = fullNameFunction.apply(personNameSwitchFunction.apply(x))
Function<PersonName, PersonName> personNameSwitchFunction = new Function<PersonName, PersonName>(){
  @Override
  public PersonName apply(PersonName from) {
    return new PersonName(from.getLastName(), from.getFirstName());
  }
};
// 関数を合成して、名前と苗字を入れ替えて、フルネームを取得
Function<PersonName, String> compose = Functions.compose(fullNameFunction, personNameSwitchFunction);
Collection<String> transform2 = Collections2.transform(personNames, compose);

for (String fullName : transform2) {
  System.out.println(fullName);
}

同じことをScalaでやってみましょう。

scala> case class PersonName(firstName:String, lastName:String)
defined class PersonName

// 名前と苗字入れ替える関数
scala> val func1 = {x:PersonName => PersonName(x.lastName, x.firstName) }
func1: (PersonName) => PersonName = <function1>

// フルネームの文字列を生成する関数
scala> val func2 = {x:PersonName => x.firstName + "." + x.lastName}
func2: (PersonName) => java.lang.String = <function1>

// 関数合成
scala> val compose = func1 andThen func2
compose: (PersonName) => java.lang.String = <function1>

// 名前と苗字を入れ替えて、フルネームまで一気に適用
scala> personNames.map(p => compose(p))
res0: scala.collection.immutable.Set[java.lang.String] = Set(KATO.Junichi, KATO.JUNICHI, Kato.JUNICHI, Kato.Junichi)
// 上記は、personNames.map(compose) と書ける

汎用性が高い関数オブジェクト集を作って組み合わせれば、DXOなんて瞬殺では?と思ったりしてます。

Javaでもその気になって頑張れば、関数オブジェクトを扱えます。まぁ、抽象度が上がってスケーラブルにプログラミングできるメリットはあるのですが、for,ifを素直に使うより可読性が落ちるが難点です。Javaでは頑張りすぎるとよくないと思うわけですw こういうのはScalaが向いてるよと。
ScalaJavaの視点で考えれるようになってきたので、いろいろアイデアが広がります。言語にも向き不向きがあるので、そのあたりをわきまえてプログラミングするってのは大事ですね。