kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

Scalaはオブジェクト指向言語です(2)

1日空いてしまいましたが、引き続きふつーのオブジェクト指向言語
としてのScalaを説明していきます。

  • 高階関数:ただのメソッド
    これはジョークではなく、本当にただのメソッドなのです。とりあえず、説明は後にして、代表的な高階関数である、mapのScala実装を見てみましょう*1

    def map[A, B](list: List[A], fun: A => B): List[B] = {
      val newList = new ArrayList[B]
      val it = list.iterator()
      while(it.hasNext()) {
        newList.add(fun.apply(it.next()))
      }
      newList
    }
    

    利用例は以下のようになります。

    val a = new ArrayList[String]{ add("A"); add("B") }
    println(map(a, (x: String) => "Hoge" + x)) //[HogeA, HogeB]
    

    さて、ここからシンタックスシュガーを全部取っ払ってみましょう。すると、次のようになります。

    def map[A, B](list: List[A], fun: Function1[A, B]): List[B] = {
      val newList = new ArrayList[B]
      val it = list.iterator()
      while(it.hasNext()) {
        newList.add(fun.apply(it.next()))
      }
      newList
    }
    
    val a = new ArrayList[String]{ add("A"); add("B") }
    val b = map(a, new Function1[String, String] {
      def apply(x: String): String = "Hoge" + x
    })
    println(b) //[HogeA, HogeB]
    


    さて、ここまで来れば、高階関数と呼ばれているものは、本当にただのメソッドだという事がおわかりいただけたかと思います。


    Scala界では、FunctionNantokaクラスのオブジェクトを引数に取るメソッドを高階関数と読んでいるだけであって、実態はこんなものです。


  • implicit parameter: ものぐさな人のための便利機能


    ここでimplicit parameterというふつーのオブジェクト指向言語には無い機能が出てきましたが、何も難しいことはありません。まず、なんでこんな機能が必要なのか、Javaプログラムを例にして説明していきます。


    まず、java.util.List<E>を引数にとって、要素の合計値を返すsumメソッドを定義したいとしましょう。定義したくないかもしれませんが、定義したいことにしてください。int型に特化した定義は次のようになります。

    import java.util.List;
    import java.util.ArrayList;
    import java.util.Arrays;
    public class SumListInt {
        public static int sum(List<Integer> list) {
          int total = 0;
          for(Integer value:list) total += value;
          return total;
        }
        public static void main(String[] args) {
          List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
          System.out.println(sum(ints));
        }
    }
    


    しかし、これではint(正確にはInteger)のListにしかsumが使えず汎用性が全くありません。そこで、インタフェースAdditiveを用意して、それを実装したクラスに対してはsumを呼び出せるようにしてみます。

    import java.util.List;
    import java.util.ArrayList;
    import java.util.Arrays;
    public class SumListAdditive {
        public interface Additive<T> {
            T add(T other);
        }
        public static <T extends Additive<T>> T sum(List<T> list) {
            T total = list.get(0);
            for(T value:list.subList(1, list.size())) total = total.add(value);
            return total;
        }
    }
    


    できました!しかし、大変残念なことに、Integerは標準ライブラリで変更しようが無いので、このsumを使うことができません。Additiveを実装したラッパークラスを作ればできないこともありませんが、無駄にコストがかさむだけでしょう。


    このアプローチの何が問題だったかと言えば、後付けで変更が効かない継承関係を前提にしてsumを実装したことにあります。


    この問題の解決法は簡単です。「二つのT型の値を足して、T型の値を返す」オブジェクトを別の引数として渡してあげれば良いのです。具体的には、次のようになります。

    import java.util.List;
    import java.util.ArrayList;
    import java.util.Arrays;
    public class SumListAddition {
        public interface Addition<T> {
            T plus(T a, T b);
            T zero();
        }
        public static final Addition<Integer> INT_ADDITION = new Addition<Integer>() {
          public Integer plus(Integer a, Integer b) {
              return a + b;
          }
          public Integer zero() {
              return 0;
          }
        };
        public static <T> T sum(List<T> list, Addition<T> addition) {
            T total = addition.zero();
            for(T value:list) total = addition.plus(total, value);
            return total;
        }
        public static void main(String[] args) {
            List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5);
    	System.out.println(sum(ints, INT_ADDITION));
        }
    }
    



  • さて、これで問題は解決…したかのように見えます。しかし、sumしたいListの型毎に適切なオブジェクトを選んで渡すのは面倒くさいです。そこで出てくるのがimplicit parameter。型に応じて、適当にうまいことINT_ADDITIONみたいなのを渡してくれる便利機能です。上記コードのScala版は次のようになります。

    import java.util.List
    import java.util.ArrayList
    import java.util.Arrays
    
    object SumListAddition {
      trait Addition[T] {
        def plus(a: T, b: T): T
        def zero: T
      }
      implicit object INT_ADDITION extends Addition[Int] {
        def plus(a: Int, b: Int): Int = a + b
        def zero: Int = 0
      }
      def sum[T](list: List[T])(implicit addition: Addition[T]): T = {
        var total = addition.zero
        val it = list.iterator()
        while(it.hasNext()) {
          total = addition.plus(total, it.next())
        }
        total
      }
      def main(args: Array[String]) {
        val ints = Arrays.asList(1, 2, 3, 4)
        println(sum(ints))
      }
    }
    


    さて、Java版との主な違いは何かというと、sumの引数additionINT_ADDITIONにimplicitという修飾子が付いたことです。


    前者は、「引数additionは、型に合うimplicit修飾子が付いたオブジェクトが見つかれば、それを勝手に渡してもらえるよ」という事を意味しています。後者がまさにそのimplicit修飾子が付いたオブジェクトです。


    ここで、「じゃあ、一体どうやってimplicit修飾子が付いたオブジェクトを探してくるんだ」という話になりますが、基本的にはimplicit parameterを持ったメソッドを呼び出している箇所から見える範囲のオブジェクトを探索します。他にちょっとややこしいルールがあったりするのですが、その辺りは普通は知らなくても大体なんとかなります。


    というわけで、第2回はこれまでです。implicit parameterの話は、導入の動機とかを考えるとどうしても長くならざるを得ませんでしたが、難しいと思われた場合、コメントいただければと思います。

*1:説明のために、あえてjava.utilのコレクションを使用しています