いまさらなのですけど、クロージャー(Closure)について

前回は、マイナー言語のClojureの、それも本質ではない部分について長々と語ってしまい、申し訳ありませんでした……。

というわけで、今日はもう少し一般的な話題を。いまさらなのですけれど、言語機能の方のクロージャー(Closure)についてやってみます。

重複は悪!

クロージャーがあって、一般的なプログラミング言語ということで、今回はC#を使ってみます。

以下のC#のコードを見てください。引数で与えられた数値の集合から、2の倍数だけを抽出するメソッド(GetMultiplesOfTwo)と3の倍数だけを抽出するメソッド(GetMultiplesOfThree)を定義しました。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  foreach (var number in numbers)
  {
    if (number % 2 == 0)
    {
      yield return number;
    }
  }
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  foreach (var number in numbers)
  {
    if (number % 3 == 0)
    {
      yield return number;
    }
  }
}

うぅ、このコードはとても気持ちが悪いです! だって、ほとんど同じ内容で、違うところは、関数名と%の後ろの数字だけなのですから。

同じコードを何回も書いて、しかもそれをずっと保守していくなんてのは、人生の無駄使いです。私の人生は死ぬまでの暇つぶしなので丸ごと無駄だという問題はさておき、とにかく重複はすぐに排除しなければなりません。

メソッドそのものを引数にとるメソッドを作成して、重複を排除する

というわけで、直します。第一段階として、2の倍数かを判断している部分と、3の倍数かを判断している部分を、メソッドに抽出します。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  foreach (var number in numbers)
  {
    if (IsMultipleOfTwo(number))
    {
      yield return number;
    }
  }
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  foreach (var number in numbers)
  {
    if (IsMultipleOfThree(number))
    {
      yield return number;
    }
  }
}

private bool IsMultipleOfTwo(int number)
{
  return number % 2 == 0;
}

private bool IsMultipleOfThree(int number)
{
  return number % 3 == 0;
}

その上で、C#ではメソッドそのもの(メソッドの実行結果ではありません。メソッド「そのもの」です)を引数にとれることを思い出し、foreachの部分全部を、判断部分を引数に取るGetNumbers()メソッドとして抽出します。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  return GetNumbers(numbers, IsMultipleOfTwo);
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  return GetNumbers(numbers, IsMultipleOfThree);
}

private IEnumerable<int> GetNumbers(IEnumerable<int> numbers, Func<int, bool> predicate)
{
  foreach (var number in numbers)
  {
    if (predicate(number))
    {
      yield return number;
    }
  }
}

private bool IsMultipleOfTwo(int number)
{
  return number % 2 == 0;
}

private bool IsMultipleOfThree(int number)
{
  return number % 3 == 0;
}

はい。これで重複がなくなりました。……が、よく考えてみると、「これ以外の他のコード」とは重複している部分があります。

だって、「集合の要素から、条件に合うものを抽出する」なんてのは、よくある処理パターンでしょ?未実装の機能一覧の中から優先順位が高い機能を抽出するとか(でも結局はハナクソみたいな絶対に誰も使わないような機能も実装するように要求されちゃうけど)、道を歩いている女性を胸部のサイズで抽出するとか(小さい方がステータスで貴重価値アリ)。

そんなよくある処理パターンなんだったら、すでに誰かがライブラリ化してくれているはずで、だから、そんな部分をわざわざプログラミングしている上のコードには、まだまだ重複があるわけです。

LINQ(のライブラリ)はとても便利!

というわけで、ライブラリを活用してみます。System.Linq名前空間のEnumerableクラスには多数の拡張メソッドが定義されていて、その一つのWhereメソッドの説明には「述語に基づいて値のシーケンスをフィルター処理します」と書いてあります。これを使ってみましょう。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  return numbers.Where(IsMultipleOfTwo);
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  return numbers.Where(IsMultipleOfThree);
}
  
private bool IsMultipleOfTwo(int number)
{
  return number % 2 == 0;
}

private bool IsMultipleOfThree(int number)
{
  return number % 3 == 0;
}

はい、とても短くなりました。素晴らしい! ……でも、上のコードには、拡張性が低いという問題があります。

だって、4の倍数を抽出したい場合には、IsMultipleOfFour()メソッドを新たに定義しなければなりません。5の倍数、6の倍数と要求が増えると、メソッドの数がどんどん増えていってしまいます。これはよくない。

クロージャーって素晴らしい!

というわけで、ここでやっとクロージャーの出番です。Wikipediaによれば、クロージャーとは「プログラミング言語における関数オブジェクトの一種。引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決することを特徴とする」なんだそうです……が、これでは何のことだか分からないので、具体的なコードを書いてみましょう。

C#において、クロージャーを作成する最も一般的な方法は、ラムダ式を定義することです。具体的なコードは以下のようになります。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  return numbers.Where(CreateIsMultipleOf(2));
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  return numbers.Where(CreateIsMultipleOf(3));
}

private Func<int, bool> CreateIsMultipleOf(int multiplier)
{
  return x => x % multiplier == 0;
}

CreateIsMultipleOf()メソッドの「x => x % multiplier == 0」の部分がラムダ式です。これは、「xという引数をとり、x % multiplier == 0の結果を返す関数を作る」という意味になります。

注意してください! ラムダ式の中でmultiplierという、外側の変数を使っています(これが「引数以外の変数を実行時の環境ではなく、自身が定義された環境(静的スコープ)において解決する」ということなんです)。CreateIsMultipleOf(2)で作成された関数では、multiplierの値は2になります。CreateIsMultipleOf(3)で作成された場合は3。4の倍数かどうかを判定する関数が欲しい場合はCreateIsMultipleOf(4)で、5の倍数の場合はCreateMultipleOf(5)で作れるのです。

ほら、これで拡張性の問題はなくなったでしょ?

更に短く

で、よくよく考えてみれば、クロージャーを作成するメソッドをわざわざ作成しないで、GetMultiplesOfTwoの中に直接ラムダ式を書いてしまえばよいわけです。更に短くします。

public IEnumerable<int> GetMultiplesOfTwo(IEnumerable<int> numbers)
{
  return numbers.Where(x => x % 2 == 0);
}
  
public IEnumerable<int> GetMultiplesOfThree(IEnumerable<int> numbers)
{
  return numbers.Where(x => x % 2 == 0);
}

もっとよくよく考えてみれば、この2つの関数も良くないです。だって、4の倍数を抽出する場合はGetMultiplesOfFour()メソッドを定義しなければならないわけで、それはIsMultipleOfFour()メソッドを定義するのと同じくらい無駄なわけです。だから、更に短くします。

public IEnumerable<int> GetMultiplesOf(IEnumerable<int> numbers, int multiplier)
{
  return numbers.Where(x => x % multiplier == 0);
}

はい、完成!30行が3行になっちゃいました。クロージャーって素晴らしいですな!