The Closure and Lambda Programming Style

Closure這種寫法,在程式語言領域存在已經有一段很長的時間了,其原意很簡單,就程式語言角度來看,Closure意指將一個function(函式)放到一個變數中,

也就是C++常用的function pointer(函式指標)的意思

/黃忠成

 

Closure or Lambda

 

  Closure這種寫法,在程式語言領域存在已經有一段很長的時間了,其原意很簡單,就程式語言角度來看,Closure意指將一個function(函式)放到一個變數中,

也就是C++常用的function pointer(函式指標)的意思,這是Closure的第一段定義,形成Closure的是重要的第二段定義,該函式必須能夠存取此function pointer形成

所在地的變數(local or global),即使所在地已經消滅,仍不影響Closure存取該所在地變數的事實。

  在我了解的語言中,JavaScript對於Closure的實作的概念與外觀算是比較直覺的,例如下面這種Closure寫法。

function test() {

    var p = function(x, y) {
             returnx + y;
    };

    alert(p(15, 20));
}

當然,你也可以寫成以下這種非Closure的樣子。

function test11() {
            alert("Hello World");
}


function test6() {
            var p = test11;
            p();
}

只是這樣就無法符合Closure的第二段定義,因此,第一種寫法在本地端具現函式的型態是必要的,下面是完整的Closure寫法。

function test2() {
        var base = 15;
        var p = function(y) {
                return base + y;
        };
       alert(p(20));
}

注意,function(y)可以存取具現所在的地的base變數,即使test2函式已經結束也不影響base變數的存取,這是Closure定義中的第二段,也是精隨部分。

所謂具現所在地,通常泛指三種型態的變數,其種兩種是local(區域)與global(全域),第三種則是由函式參數傳導過來而形成的local(區域)變數。

function test3(base) {
     var p = function(y) {
                returnbase + y;
     };
     alert(p(20));
}

Closure也可以當成參數傳入。

function test6(myFunc) {
            alert(myFunc(15, 25));
}

.................

test6(function (x, y) {
        return x + y;
        });

  在JQuery誕生之前,我們很少使用這種手法去撰寫程式,因為這種方法有個觀念上的缺失,那就是函式本身是為了切割程式複雜度及可重用而設計的,Closure破壞了這個觀念,

這是為何在JQuery誕生之前我們很少看到這種寫法的主要原因。

  任何應用程式都有著可重用性的部分,但相對的也存在著一次性、可拋棄性的部分,舉例來說,當為button撰寫一個click事件處理函式時,可能會是這樣寫的。

 IE Only

<button name="testMyButton" type="button">variable catch2</button>

<script language="javascript">
    function myButtonClick() {
        alert("Hello World");    
    }
    testMyButton.onclick = myButtonClick;
</script>

只是,myButtonClick其實就只用在testMyButton,根本不具備可重用性的價值,透過Closure,我們可以少打些字,並讓程式看起來更平述些。

<button name="testMyButton" type="button">variable catch2</button>

<script language="javascript">
    testMyButton.onclick = function () {
        alert("Hello World");
    }
</script>

在C# 2.0時,新增了Anonymous Method的寫法,也就是匿名函式,這是C#最早所支援的Closure寫法。

public delegate void SumPrint(int x, int y);
public delegate void SumPrint2(int y);

public static void Test()
{
  SumPrint p = delegate(int x, int y)
  {
     Console.WriteLine(x + y);
  };
 p(15, 20);
}

Anonymous Method其實也具備了Closure第二段定義的能力。

public delegate void SumPrint(int x, int y);
public delegate void SumPrint2(int y);

public static void Test2()
{
     int baseValue = 15;
     SumPrint2 p = delegate(int y)
     {
         Console.WriteLine(baseValue + y);
     };
     p(15);
}

在C# 3.0加入了新名詞: Lambda Expression,這是一個原本來自數學運算式表示方法的詞,在C# 3.0中寫法是這樣的。

public static void Test5(Action printFunc)
 {
      printFunc("Hello World");
 }
………
static void Main(string[] args)
{           
        Test5(s => Console.WriteLine(s)); 
           Console.ReadLine();
}

原本的Anonymous Method寫法則與新的Lambda Expression寫法結合,形成另一種更容易使用的Closure寫法。

public static void Test6()
{
            int baseValue = 15;
            var p = new Action((y) =>
            {
                baseValue += 5;
                Console.WriteLine(baseValue + y);
            });
            p(15);
}

也可以當成參數傳遞。

 

public static void Test5(Action printFunc)
{
            printFunc("Hello World");
}

.......................

Test5(s => Console.WriteLine(s));

 

如需傳入有傳回值的參數則是使用Func<T...>。

 

Closure in Java

 

  相對於JavaScript與C#,Java對Closure的支援就較為薄弱,比較不那麼簡單易懂,但也不能說沒有,其以Anonymous Class型態呈現。

public static void Test() {
   Thread th = new Thread(new Runnable()
    {
	  @Override
	  public void run() {
	 	System.out.println( "test");
	  }
    }); 
    th.start();
}

當然,也可存取所在地變數。

public static void Test2() {
  final String baseString = "Hello ";
  Thread th = new Thread(new Runnable()
   {
     @Override
     public void run() {
            System.out.println( baseString + "test");
      }
    });
  th.start();
}

特別注意一下這段程式,這裡要存取baseString必須將其設定為final,也就是不可再次變更她的值,如果想這樣做,必須將

baseString提出成為類別變數才行。

public class TestClosure {

   private static String g_baseValue = "Hello! ";
……………
   public static void Test4() {		
		Thread th = new Thread(new Runnable()
		{
			@Override
			public void run() {
				g_baseValue = g_baseValue +"test";
				System.out.println( g_baseValue);
			}
		}
		);
		th.start();
    }

這指出了實作Closure必須注意的另一個問題,那就是存取所在變數分成兩部分,一個是讀,這點在多數語言都可以辦到,但另一個寫的動作在不同語言中會有不同行為。

另外,補充一點,JDK 1.8會支援更簡潔、用途更廣的的Closure寫法。

 

Closure in C++

 

   C++ 0x(我知道她正名為C++ 11了,但我喜歡C++ OX這個詞),正式支援Lambda寫法,也就是Closure,下列的寫法可以在支援C++ 11的編譯器上正常運作。

#include 
#include 
#include 

using namespace std;

void test1()
{
    auto func1 = [](int x, int y) { return x + y; };
    cout << func1(15,20) << endl;
}

在存取所在地變數部分,C++ 11做出更明確的規則。

[]

不捕捉任何所在地變數,也就是說function body中不能存取任何所在地變數,是完全封閉的函式,前面列出的寫法就是這種。

[=]

以唯讀方式捕捉所有所在地變數,function body不允許更改變數的值。

[&]

以傳址方式捕捉所有所在地變數,function body允許更改變數的值。

[<variable name>]

明確指定捕捉哪一個所在地變數。

[this] 捕捉this指標。

[]方式最簡單,就是都不捕捉就是了,第二種是唯讀方式捕捉,例如下面這樣。

void test7(int x,int y)
{
    auto func1 = [=]()
    {
        return x + y;
    };
    cout << func1() << endl;
}

此時function body中不允許對x或y變數賦值動作發生,就如同Java一樣。

 Error!

auto func1 = [=]()
    {
        x = x +7;
        return x + y;
    };

要做到賦值,得用[&]。

auto func1 = [&]()
    {
        x = x +7;
        return x + y;
    };

最後一種是直接指定變數來補捉。

void test2()
{
    int x = 15;
    auto func1 = [x](int y) { return x + y; };
    cout << func1(20) << endl;
}

你也可以指定捕捉一個以上的變數,或是指定特定變數以傳址方式補捉。

void test4(int x,int y)
{
    auto func1 = [&x,y]()
    {
        x++;
        return x + y;
    };
    cout << func1() << endl;
}

當沒有加上&時,就是以=方式捕捉。

C++在宣告Lambda時,允許明確指定函式的回傳值,如果不指定,就是交由編譯器決定。

void test4(int x,int y)
{
    auto func1 = [&x,y]() -> int
    {
        x++;
        return x + y;
    };
    cout << func1() << endl;
}

當賦值的變數可能影響回傳值時,某些編譯器(例如C++/CX,Visual C++ 11)需要明確指定回傳值的型別,

也就是 -> <型別>。

 

當成參數傳遞的寫法如下。

void test6(function<int (int,int)> func)
{

cout << func(15,20) << endl;
}
................

test6([](int x,int y) { return x + y;});

 

 

 

Closure in Objective-C

 

Objective-C 2.0開始引入Blocks概念,對應了Closure寫法。

void test1()
{
    int (^Sum)(int,int) = ^(int x,int y) {
        return x + y;
    };
    NSLog(@"%d", Sum(15,20));
}

也可存取所在地變數。

void test2()
{
    int baseValue = 20;
    int (^Sum)(int) = ^(int y) {
        return baseValue + y;
    };
    NSLog(@"%d", Sum(20));
}

變數捕捉部分預設為傳值,可使用__block宣告式改變為傳址。

void test3()
{
    __block int baseValue = 15;
    int (^Sum)(int) = ^(int y) {
        baseValue += 5;
        return baseValue + y;
    };
    NSLog(@"%d", Sum(20));
}

當成參數的寫法如下。

typedef int (^SumFunc)(int,int);

void test4(SumFunc func)
{  
    NSLog(@"%d", func(20,50)); 
}

...............

test4( ^(int x,int y) {
            return x + y;
        });

 

Closure or not?

 

  當一種手法在誕生時不獲得重視時,必然有其背景因素存在,當年AJAX不被重視是因為網路速度及背景技術沒辦法支持這種寫法,Closure不被重視則是因為

函式本身就是用來切割複雜度及提高可用性的。

  另外,Closure通常會帶出值捕捉的議題,假設以下這種C++寫法,你能猜出結果如何嗎?

void test7(int x,int y)
{
    auto func1 = [&]()
    {
        x = x +7;
        return x + y;
    };
    auto func2 = [&]()
    {
        return x + y;
    };
    cout << func1() << endl;
    cout << func2() << endl;
}
….
test7(15,20);

答案是兩個都是42。

換一種寫法。

void test7(int x,int y)
{
    auto func1 = [&]()
    {
        x = x +7;
        return x + y;
    };
    auto func2 = [&]()
    {
        return x + y;
    };
    cout << func2() << endl;
    cout << func1() << endl;
}

答案是35及42,這就是重點,因為傳址式捕捉行為,會讓程式變得難以預測,所以非到必要時不要使用傳址式補捉。

雖然C# 沒有明確指定值捕捉行為的語法,但有個隱規則存在,對於區域變數(值(Value)型態變數)預設為傳值式捕捉,類別變數與靜態變數則是傳址方式捕捉。

public static void Test7(int x,int y)
{
            var p = new Func((int x1,int y1) =>
            {
                x1 += 7;
                return x1 + y1;
            });
            var p1 = new Func((int x1, int y1) =>
            {
                return x1 + y1;
            });
            Console.WriteLine(p(x,y).ToString());
            Console.WriteLine(p1(x, y).ToString());
}

 

private static int g_x = 15;
public static void Test8(int y)
{
            var p = new Func(( int y1) =>
            {
                g_x += 7;
                return g_x + y1;
            });
            var p1 = new Func((int y1) =>
            {
                return g_x + y1;
            });
            Console.WriteLine(p(y).ToString());
            Console.WriteLine(p1(y).ToString());
}

對於參考型態變數則一律為傳址式捕捉。

public static void Test9(MyValue x)
 {
             var p = new Func((MyValue x1) =>
             {
                 x1.x += 7;
                 return x1.x + x1.y;
             });
             var p1= new Func((MyValue x1) =>
             {
                 return x1.x + x1.y;
             });
             Console.WriteLine(p(x).ToString());
             Console.WriteLine(p1(x).ToString());
 }
 …………………
 public class MyValue
 {
         public int x;
         public int y;
 }

 


JavaScript中沒有傳值捕捉的概念,一律為傳址。

var g_base = 15;
function test4() {
            var p = function (y) {
                g_base += 7;
                return g_base + y;
            };
            var p1 = function (y) {
                return g_base + y;
            };

            alert(p(20));
            alert(p1(20));
}

 

後記

 

  Closure是把雙面刃,一般來說其實不太適合程式初學者使用,只是因為時代的變遷而浮上檯面,在使用前,請記得注意值捕捉及重用性的課題,

本文所列出的也非所有情況,畢竟值捕捉搭上物件參考或是指標時,變化性非常多樣 。

PS: 本文中,我儘量解釋5種語言的傳址與傳值捕捉行為,若亂中有錯,也請指出,謝謝。