Perl入門ゼミ

テキスト処理、Linuxサーバー管理、Web開発ならPerl
  1. Perl
  2. サブルーチン
  3. here

クロージャーを作成する方法

Perlで「クロージャー」を作成する方法を解説します。

クロージャの定義

クロージャの定義をコードを使って解説します。

use strict;
use warnings;

# もっともシンプルなクロージャ
{
  # 変数の生成
  my $var = 5;
  sub var {
    return $var;
  }
}
# スコープが終わってもvarサブルーチンに参照されているため、
# $var は存在し続ける。
# var サブルーチンだけが、$var を見ることが可能。

print "1: もっともシンプルなクロージャ\n";
print var(), "\n";

クロージャの定義

クロージャであるための条件は、サブルーチンが、自分のスコープ外のレキシカル変数を参照していることです。( この例では、var が、自分のスコープ外の$var を参照。)

クロージャの定義は人や状況によってあいまいです。varサブルーチンのことをクロージャと呼ぶこともありますし、「varサブルーチンと参照されている変数$var を含む環境」のことをクロージャと呼ぶこともあります。

サブルーチンを作成するときは、必ず引数として変数を受け取って、スコープ外の変数を参照してはいけないと書きましたが、クロージャは例外のひとつです。

Perlのスコープについては以下の記事を参考にしてください。

クロージャの特徴

{
  # 変数の生成
  my $var = 5; 
  sub var {
    return $var;
  }
}

$var は、varサブルーチン内で参照されているために、スコープが終了しても、メモリ上から削除されない。( レキシカル変数は、スコープが終わって、どこからも参照されていないときにメモリ上から破棄される。)

スコープが終了すると、スコープ外からは、$var が見えなくなる。( varサブルーチンは、スコープの影響を受けないため、スコープ外からは見える。)

$var を見ることができるのは、var サブルーチンだけである。スコープが終了した後は、var サブルーチンを通してのみ、 $var の値にアクセスすることができる。

クロージャのイメージ

|-------------スコープ( クロージャ )--------------|
|                                                 |
| |--------|              |--------------|        |     |------------|
| |        | アクセス可能 |              | アクセス可能 |            |
| | データ |<-------------| サブルーチン |<-------------| スコープ外 |
| |        |              |              |        |     |            |
| |--------|              |--------------|        |     |------------|
|                                                 |
|-------------------------------------------------|

                   スコープ外からデータにはアクセス不能

スコープ外からは、サブルーチンを通じてのみデータにアクセス可能。直接データにアクセスすることができない。

クロージャを「関数ジェネレータ」を使って作成する

クロージャを関数ジェネレータを使って作成してみます。

use strict;
use warnings;

# タイマーを生成する。
print "1: 関数ジェネレータでクロージャを生成する( タイマー )。\n";
my $timer1 = create_timer(); 
sleep 1;
# 1秒後にもうひとつタイマーを生成
my $timer2 = create_timer();

# 1秒止まる
sleep 1;

print '$timer1 による経過秒: ';
print $timer1->(), "\n";

print '$timer2 による経過秒: ';
print $timer2->(), "\n\n";


# 経過時刻を知るクロージャを生成する関数ジェネレータ
sub create_timer {
  # time() は、関数ジェネレータが呼び出された時刻
  my $time = time(); 
  
  return sub {
    # time() は生成されたサブルーチンが
    # 呼び出された時刻。time() - $time で
    # 経過時刻を知ることができる。
    return time() - $time;                          
  }
}

クロージャを生成するための関数ジェネレータ

sub create_timer {
  # time() は、関数ジェネレータが呼び出された時刻
  my $time = time(); 

  # time() は生成されたサブルーチンが
  # 呼び出された時刻。time() - $time で
  # 経過時刻を知ることができる。
  return sub {
    return time() - $time;                      
  }
}

関数ジェネレータとは、サブーチンを生成するサブルーチンのことです。関数ジェネレータを利用すれば、複数のクロージャを生成することができます。

生成されるそれぞれのクロージャは、データを独自に持ちます。この例では、 $time の値は、メモリ上にクロージャ毎に作成されます。戻り値はサブルーチンへのリファレンスになっています。これは、Perlでは、サブルーチンそのものを戻り値にすることができないためです。

関数ジェネレータの呼び出してクロージャを作成する

my $timer1 = create_timer();
my $timer2 = create_timer();

戻り値は、サブルーチンへのリファレンスになります。このサブルーチンはクロージャで,独自にメモリ上に環境( サブルーチンに参照された変数とサブルーチンの定義 )を持ちます。

クロージャを呼び出す

$timer1->();

サブルーチンへのリファレンスをうけったので、デリファレンスして呼び出します。

クロージャの利用方法

クロージャを利用すると、「基準になる時点での状態」と「現時点での状態」の変化を簡潔に記述することができます。

クラスを作れば同じことを実現できますが、クラスを作りたくない場合は、クロージャを利用します。( クラスをひとつ作れば、クラスを管理する手間が増えます。この例のようにシンプルな記述のためにわざわざクラスを作りたくない場合に、クロージャを使います。 )

クロージャに参照される「変数を初期化」する

クロージャに参照される変数を初期化するサンプルです。

use strict;
use warnings;

# 関数ジェネレータに引数として渡す。
print "1: クロージャに参照される変数を初期化する。\n";
my $sign_inversion = create_sign_checker(1); 

if ($sign_inversion->(-2)) {
  # 符号が反転していたら
  print "-2 は、1から見て符号が反転しています。\n";
}

# 符号が反転しかかどうかを確かめるクロージャを生成する関数ジェネレータ
sub create_sign_checker {
  # クロージャを作成したときに初期化される。
  my $num1 = shift; 
    
  return sub {
    # クロージャを呼び出したときの引数。
    my $num2 = shift; 
    
    if ($num1 * $num2 > 0) {
      # 反転していない場合
      return 0; 
    }
    elsif ($num1 * $num2 < 0) {
      # 反転している場合
      return 1; 
    }
    else {
      # どちらかが0の場合undefを返す
      return; 
    }
  }
}

クロージャに参照される変数を初期化する

# クロージャを生成。( クロージャに参照される変数を1で初期化 )
my $sign_inversion = create_sign_checker(1);

# クロージャを呼び出す
$sign_inversion->(-2);

# クロージャを生成する関数ジェネレータ
sub create_sign_checker {
  # クロージャを作成したときに初期化される。
  my $num1 = shift; 
    
  # クロージャの定義
  return sub{
    # クロージャを呼び出したときの引数。
    my $num2 = shift; 
    # ...
  }
}

クロージャを作成するときに、クロージャが参照する変数を初期化することができます。クロージャを作成するときに引数として渡します。それを、関数ジェネレータの引数として受け取って、変数に設定します。

クロージャの概念をクラスとの対比でわかりやすく説明する

クロージャの概念はぼやっとしていて輪郭を捕らえるのが難しいです。わたしなりにできるだけ理解できるように解説したいと思います。

クロージャとは

  1. クロージャーとは、データとそのデータを操作する関数のセットである。
  2. データはカプセル化されていて、データを操作できるのは、クロージャ内部の関数だけである。
  3. データの初期化が可能である。
  4. データごとに個々のインスタンスを作成することができる。
  5. 関数はインスタンスから呼び出される。

次に、オブジェクト指向におけるクラスについて。

クラスとは

  1. クラスとは、データとそのデータを操作する関数のセットである。
  2. データはカプセル化されていて、データを操作できるのは、クラス内部の関数だけである。
  3. データの初期化が可能である。
  4. データごとに個々のインスタンスを作成することができる。
  5. 関数はインスタンスから呼び出される。

ここで気づいてほしいことは、クラスとクロージャは、実は同じものだということです。Perlの実装においては、クラスとクロージャは、文法的な書き方において違いがあります。けれど、機能的には同じものです。クロージャとは簡易なクラスのことだと思えば理解しやすいです。(クロージャの概念は、もう少し広いけれど、最初のうちは簡易なクラスだと理解する。)

クラスとクロージャの相違点

  1. クロージャは、クラス名を持たない。my $ins = Class->new; というnewを呼び出すことで、インスタンスを生成するのではなく、 my $ins = closure_maker(); という形でインスタンスを生成する。
  2. クロージャは、継承できない。クラスのように継承をすることはできません。

クラスとクロージャを使って、数を数えるための機能(カウンター)を実装してみます。

カウンターをクラスで実装する

use strict;
use warnings;

package Counter;
sub new{
  my $class = shift;
  my $self = {};
  $self->{cnt} = shift;
  bless $self, $class;
}

sub add_count{
  my $self = shift;
  $self->{cnt}++;
}

sub count{
  my $self = shift;
  return $self->{cnt};
}

package main;
my $cnt = Counter->new(10);

print "1: クラスによるカウンターの実装\n";
print "カウント前:", $cnt->count, "\n";
$cnt->add_count;
print "カウント後:", $cnt->count, "\n\n";

カウンターをクロージャで実装する

use strict;
use warnings;

# クロージャ
sub make_counter{
  my $self = {};
  $self->{cnt} = shift;
   
  # クロージャでは、サブルーチンへのリファレンスを使ってメソッドを実装する。
  my $funcs = {}; 
  $funcs->{add_count} = sub {
    $self->{cnt}++;
  };
   
   $funcs->{count} = sub {
     return $self->{cnt};
   };
   return $funcs;
}

my $cnt = make_counter(10);

print "2: クロージャによるカウンターの実装\n";
print "カウント前:" . $cnt->{count}->() . "\n"; 
$cnt->{add_count}->();
print "カウント後:" . $cnt->{count}->() . "\n";

このようにクラスとクロージャは、よく似ています。次回は、コードを解説します。

クラスとクロージャの視覚的なイメージ

## クラス
|------クラス------|
| ・データ         |
| ・データ操作関数 |
|------------------|


## クロージャ
|----クロージャ----|
| ・データ         |
| ・データ操作関数 |
|------------------|

小飼 弾さんの添削

  • perl - Class vs. Closure(404 Blog Not Found)
  • クロージャでクラスと同じことができるけれど、Perlではクロージャの生成は、オブジェクトの生成よりもコストがかかるそうです。
  • クロージャは、継承できないと書きましたが継承できるみたいです。(やりかたは今のところは思いつきません。)
Qiitaで
「3分間Perlテキストクッキング」
という連載を始めました。
テキスト処理を題材にして、3分くらいで読める分量で、書いていきます。
文字コード、テキストデータ、コンピュータにおけるテキストの扱いなど、ソフトウェアの基礎の話題も
3分間Perlテキストクッキング