各言語におけるクロージャー

Liyad v0.0.13をリリースしました。 今回のリリースでは、クロージャー(関数閉包)関連のオペレーターが追加になりました。

($closure (arg1 argN) use (var1 varM) expr)
(|-> (arg1 argN) use (var1 varM) expr)

($capture (var1 varM) ($lambda (arg1 argN) expr))
($capture (var1 varM) (-> (arg1 argN) expr))

$closure および |->クロージャーとしてラムダ式を作成します。これは、$capture$lambda または -> を組み合わせた式と等価です。
$capture は、その括弧内において、引数で指定されたシンボルを括弧内で定義される関数およびラムダ式のスコープに引き渡します。 指定されたシンボルでアクセスされる変数は、関数およびラムダ式の中から参照および変更が可能です。

Liyadでは上述の文法を取りますが、他の言語においてはどのような文法を取るのか調べてみました。


Liyad

($let fn ($local ((counter 0))
    (|-> () use (counter) ($set counter (+ counter 1))) ))

(fn)

クロージャーにキャプチャーさせる変数を明示することで、 $local のスコープにアクセスできます。
原則、関数内部からは自身の関数スコープとグローバルスコープ以外にアクセスできません。
但し、 $defun はスコープに影響されません (常にグローバルです)。
Liyadは Lisp-2 であるため、 $defun された関数の名前空間$let で定義される変数の名前空間は独立しています。

Liyadでは、通常、実行時のスタックでしかスコープを管理していませんが、字句解析時に上記オペレーターによってキャプチャーされたシンボルがある場合は、 該当スコープへの参照を束縛させるようにしています。

Common Lisp

(setf fn (let ((counter 0))
    (lambda () (incf counter)) ))

(fn)

特に意識することなく、let のスコープから抜けた後、fn を通して let のスコープにアクセスできます。
すべてのラムダ式は、字句解析時のスコープ(レキシカルスコープ)を記憶しています。

JavaScript

let fn = void 0;
{
    let counter = 0;
    fn = () => ++counter;
}

fn();

こちらも、自動的にブロックスコープにアクセスしています。
Common Lisp同様、すべてのラムダ式は、字句解析時のスコープ(レキシカルスコープ)を記憶しています。
他の例と記述を合わせて、即時関数で書くと次のようになります。

const fn = (() => {
    let counter = 0;
    return (() => ++counter);
})();

fn();

Python

def makeFn():
    counter = 0
    def inner():
        nonlocal counter
        counter = counter + 1
        return counter
    return inner
fn = makeFn()

fn()

クロージャー内で外側の変数について値を変更するためには、nonlocal で明示的に示す必要があります。

PHP

<?php

$fn = (function() {
    $counter = 0;
    return (function() use (&$counter) {
        return ++$counter;
    });
})();

$fn();

use でキャプチャーする変数を指定する必要があります。

C#

using System;
namespace MyApp
{
    public class MyClass 
    {
        public static Func<int> MakeFn()
        {
            int counter = 0;
            return () => ++counter;
        }

        static void Main(string[] args)
        {
            var fn = MakeFn();
            fn();
        }
    }
}

Java

Javaクロージャーは「パチモン」と謂われています。

public class MyClass {
    public static Supplier<Int> makeFn() {
        final int[] counter = {0};
        return () -> { return ++(counter[0]); };
    }

    public static int main(String[] args) {
        Supplier<Int> fn = makeFn();
        fn();
    }
}

final または、 実質的に final (初期化後、代入されているオブジェクト参照またはプリミティブ値が変更されない) ことが、 クロージャーに変数を引き渡せる条件となっています。
つまり、実装としてはスコープを渡しているのではなく、単に見えない引数として渡しているということです(つまり、関数の部分適用ですよね)。

クロージャーの定義は、 関数の定義された環境(静的スコープ)の変数への 参照を束縛 していることなので「パチモン」と謂われるわけです。

ただ、変数への再代入を認めないタイプの関数型言語では、部分適用による新しい関数の生成、つまり 値の束縛 による環境の引き渡しを クロージャーであるとされる場合もあるので、これが絶対にクロージャーではないとも言えないですね。

C++

C++11 でラムダ式クロージャーをサポートしたんですね (最近、C++ 書いてないので…)。

#include <functional>
int counter = 0;
std::function<int()> fn = [&counter] () -> { return ++counter; };

int main(int argc, char* argv[]) {
    fn();
}

変数のキャプチャーと動作(コピーor参照)を指定する必要があります。
クロージャーを作ったからと言って、関数スコープの寿命が変更されるわけではないので、auto変数を参照した場合、 スコープを抜けた後の動作は未定義です。

#include <functional>

std::function<int()> makeFn() {
    int counter = 0;
    return [&counter] () -> { return ++counter; };
}

int main(int argc, char* argv[]) {
    auto fn = makeFn();
    fn(); // 未定義!
}

参考