Processingで無名関数的なことしてみた


はじめに

自分は大学でProcessingを勉強し始め、その後Rust言語を使うようになりました。まあProcessingは狭い世界で、やれることが全然少ない!Rust最高!となったわけですが、Processingも長いこと使っていて愛着があるので、なにかProcessingにはない機能を再現してみたいと思いました。その一つが無名関数です。

無名関数が欲しくなったワケ

事の発端は、Processingで素数を求めるプログラムを作成していた時です。すぐに完成したのですが、Rustならもっと短くかけるのでは?と思い実装してみました。以下がこれらのコードです。

// Processing

ArrayList<Integer> exception_list = new ArrayList();
loop: for(int n=2; n<100; n++){
  for(int m: exception_list){
    if(n % m == 0){ continue loop; }
  }
  println(n);
  exception_list.add(n);
}
// Rust

let mut exception_list = vec![];
'Loop: for n in 2..100 {
    if exception_list.iter().any(|m|n % m == 0) { continue 'Loop; }
    println!("{}", n);
    exception_list.push(n);
}

アルゴリズムが簡単なのでそこまでの変化ではありませんでしたが、Rustの場合、簡単に説明すると「配列の全要素に対して、それを引数にとってbool値を返す関数を適用させ、その中の一回でもtrueが返った場合に」trueを返すany()というメソッドがあります。
Processingでは無名関数(クロージャやラムダ式とも言います)が使えないのですが、これと同じことはできないかと考えていました。

インスタンス化と同時に関数を定義

Processingではnew ClassName(...)によりインスタンスを作りますが、その際に関数のオーバーライド(abstractクラスに関しては定義)ができます。書いている途中で思い出しましたが、リスナーを登録するときにも使われていますね。

abstract class MyClass {
  abstract void func();
}

void setup() {
  MyClass c = new MyClass(){
    public void func(){ // publicでなければエラー
      println("Hello, world!");
    }
  };
}

そこで以下のようにして、機能だけは無名関数のようなものを実装してみました。

abstract class fn<R, A> {
  abstract R func(A arg);
}

class Vec<T> {
  ArrayList<T> list = new ArrayList();

  void add(T item) {
    this.list.add(item);
  }

  boolean any(fn f) {
    for(T item: this.list){
      if( (boolean)f.func(item) ){ return true; }
    }
    return false;
  }
}

void setup() {
  Vec<Integer> vec = new Vec();
  for(int n=2; n<100; n++){
    final int _n = n;
    if(vec.any(new fn<Boolean, Integer>(){
      public Boolean func(Integer m){ return _n % m == 0; }
    })){
      continue;
    }

    println(n);
    vec.add(n);
  }
}

...前より長いです。しかも書きづらいです。だから機能だけは無名関数なんです。
では解説していきます。

abstract class fn<R, A> {
  abstract R func(A arg);
}

まずは無名関数というものを定義していきます(定義した時点で無名じゃないというツッコミはNGです)。
abstractキーワードにより、このクラスは関数を定義させなければ使えなくなりました。必須のコードではありませんが、このクラスのfunc関数はデフォルト実装をもたせたくないので悪くはないかなと。
<R, A>の部分なのですが、これはジェネリクスというもので、動的に型を決定できます。「なにかしらの型ではあるが、今はまだわからない。とりあえず名前だけは作っておくから使えるよ。」といった意味でとらえていただければ問題ありません。

class Vec<T> {
  ArrayList<T> list = new ArrayList();

  void add(T item) {
    this.list.add(item);
  }

  boolean any(fn<Boolean, T> f) {
    for(T item: this.list){
      if( f.func(item) ){ return true; }
    }
    return false;
  }
}

ProcessingのArrayListはもちろんですがRustで使われているようなanyメソッドを持っていません。ので、ラッパークラス的なものを作成していきます。ここでもジェネリクスが用いられており、ArrayList同様どんな型に対してもインスタンスを作成できます。これはanyメソッドを持ったArrayListと考えてください。
ここて定義されているanyメソッドは、型パラメータがBooleanT(このVecクラスで用いているTと同じ型)であるfnクラスのオブジェクトを引数にとり、fという名前で引数変数を作っています。拡張for文(foreachみたいなもの)によって渡されたリストの中身を引数として、func関数を呼び出しています。ただ、func関数はまだ定義していませんでしたね?これは、引数として与える際に実装しています。次をご覧ください。

void setup(){
  Vec<Integer> vec = new Vec();
  for(int n=2; n<100; n++){
    final int _n = n;
    if(vec.any(new fn<Boolean, Integer>(){
      public Boolean func(Integer m){ return _n % m == 0; }
    })){
      continue;
    }
    println(n);
    vec.add(n);
  }
}

Integer型の可変長配列が欲しいので、Vec<Integer>という型のインスタンスを作ります。
変数vecに対してこの部分5行目でanyメソッドを使っており、引数にはfnクラスのインスタンスを渡しています。ここでfunc関数を定義を行なっています。
Vec<Integer>型のオブジェクトvecに実装されているanyメソッドの引数は、TIntegerに決定したのでfn<Boolean, Integer>となります。したがって、ここで作られるfnクラスの型パラメータも合わせて設定しないといけません。同時に、実装するfunc関数の引数と返り値の型も正しく揃えます。
定義したfunc関数は単純で、_nという整数を与えられた整数で割り切れるか否かを返しているだけです。この_nなのですが、単純にnを渡そうとするとエラーになってしまいます。しょうがなくエラー回避のためにfinal宣言をした変数_nを使用しています。

あとは同じですね。結果的にそれまで求めた素数で割り切れなかった場合に、出力してからリストに加えています(このアルゴリズムをエラトステネスの篩といいます)。

まとめ

外で定義されている変数を使うのに一手間必要だったり(そもそもfinal宣言することでできなくなることもあるのでは...)、無名関数と言いつつ無名じゃなかったりと、実用的かと言われれば微妙ではありますが、このようにして関数を渡すこともできるという記事でした。