Có gì mới trong PHP 5.3, Phần 2: Bao đóng và các hàm lambda

Tiếp theo với loạt bài viết "Có gì mới trong PHP V5.3" là bài với các tính năng mới và hứng thú được thấy trong PHP V5.3, đã lên lịch phát hành sớm. Phần 1 xem xét các thay đổi đối với việc lập trình hướng đối tượng và xử lý đối tượng trong PHP V5.3. Trong Phần 2 ở đây, chúng ta xem các bao đóng và các hàm lambda. Chúng được thiết kế để lập trình dễ hơn nhiều bằng cách cho phép bạn dễ dàng xác định các hàm sử dụng một lần (throwaway functions), có thể dùng trong nhiều ngữ cảnh.

John Mertic, Kỹ sư phần mềm, SugarCRM

John Mertic đã tốt nghiệp đại học ngành Khoa học Máy tính của Đại học Quốc gia Kent và hiện là kỹ sư phần mềm tại SugarCRM. Ông có nhiều đóng góp vào các dự án mã nguồn mở, đáng kể nhất là các dự án PHP; ông là người tạo ra và duy trì Bộ Cài đặt PHP trên Windows (PHP Windows Installer).



06 11 2009

Các khái niệm về bao đóng và hàm lambda chắc chắn không phải là mới; cả hai đều từ giới lập trình hàm (functional programming). Lập trình hàm là một kiểu lập trình mà chuyển trọng tâm từ khai thác câu lệnh sang đánh giá biểu thức. Các biểu thức này được hình thành bằng cách sử dụng các hàm, chúng được kết hợp với nhau để có các kết quả mà chúng ta có thể đang tìm kiếm. Kiểu lập trình này thường được sử dụng nhiều hơn trong một môi trường hàn lâm, nhưng cũng thấy trong các lĩnh vực của trí tuệ nhân tạo và toán học, và có thể thấy trong các ứng dụng thương mại với các ngôn ngữ như Erlang, Haskell, và Scheme.

Các bao đóng lúc đầu được phát triển trong những năm 1960 như là một phần của Scheme, một trong những ngôn ngữ lập trình hàm nổi tiếng nhất. Các hàm lambda và bao đóng thường gặp trong các ngôn ngữ mà các hàm được xử lý như là các giá trị cấp 1, nghĩa là các hàm có thể được tạo ra ngay và được chuyển như tham số sang ngôn ngữ khác.

Kể từ đó, các hàm bao đóng và lambda đã tìm thấy đường đi ra ngoài giới lập trình hàm và thành các ngôn ngữ như JavaScript, Python, và Ruby. JavaScript là một trong những ngôn ngữ phổ biến nhất hỗ trợ các hàm bao đóng và lambda. Nó thực sự sử dụng chúng như là một phương tiện để hỗ trợ lập trình hướng đối tượng, trong đó các hàm được lồng vào các hàm khác để hoạt động như là các thành viên riêng. Liệt kê 1 đưa ra một ví dụ về cách JavaScript sử dụng các bao đóng.

Liệt kê 1. Một đối tượng JavaScript được xây dựng nên bằng cách sử dụng các bao đóng
var Example = function()
{ 
    this.public = function() 
    { 
        return "This is a public method"; 
    }; 

    var private = function() 
    { 
        return "This is a private method"; 
    };
};

Example.public()  // returns "This is a public method" 
Example.private() // error - doesn't work

Như chúng ta đã xem trong Liệt kê 1, các hàm thành viên của đối tượng Example được định nghĩa như các bao đóng. Do phương thức riêng bó hẹp trong một biến cục bộ (trái với phương thức chung gắn với đối tượng Example bằng cách sử dụng từ khoá này), nó không thể hiện đối với thế giới bên ngoài.

Bây giờ chúng ta đã thấy một vài quan điểm lịch sử về nơi xuất xứ khái niệm này, hãy quan sát các hàm lambda trong PHP. Khái niệm về các hàm lambda là cơ sở cho các bao đóng và cung cấp cách cải tiến hơn nhiều để tạo ra các hàm khi thực hiện, trái với hàm create_function() có sẵn từ trước trong PHP.

Các hàm lambda

Các hàm lambda (hay “hàm ẩn danh” như chúng thường dẫn) chỉ đơn giản là các hàm sử dụng một lần, có thể được định nghĩa vào bất cứ lúc nào, và thường gắn với một biến. Các hàm tự chúng chỉ tồn tại trong phạm vi của biến mà chúng được định nghĩa, vì vậy khi biến đó vượt ra ngoài phạm vi, thì hàm này cũng không còn. Quan niệm về các hàm lambda có từ công trình toán học trước những năm 1930. Được hiểu như là một phép tính lambda, nó được thiết kế để kiểm tra định nghĩa và ứng dụng hàm, cũng như là khái niệm về phép đệ quy. Sản phẩm từ phép tính lambda được sử dụng để phát triển các ngôn ngữ lập trình hàm, như Lisp và Scheme.

Các hàm lambda rất tiện dụng cho một số cá thể, đáng chú ý nhất đối với nhiều hàm PHP mà chấp nhận một hàm gọi lại (callback function). Một hàm như vậy là array_map(), nó cho phép chúng ta đi qua một mảng và áp dụng một hàm gọi cho từng yếu tố của mảng. Trong các bản trước của PHP, vấn đề lớn nhất với các hàm này là ở chỗ không có một cách sáng sủa nào để xác định hàm gọi lại; chúng ta đã bị tắc ở chỗ nhận một trong ba cách tiếp cận vấn đề sẵn có:

  1. Chúng ta có thể định nghĩa hàm gọi lại ở một nơi nào đó trong mã để chúng ta biết nó là sẵn có. Việc này là tồi do nó chuyển một phần thực hiện cuộc gọi ở nơi khác, mà lại khá bất tiện cho việc đọc và bảo trì, đặc biệt là nếu chúng ta không định sử dụng hàm này ở một nơi khác.
  2. Chúng ta có thể định nghĩa hàm gọi trong cùng một khối mã, nhưng phải có tên. Trong khi việc này giúp giữ các thứ với nhau, chúng ta cần phải thêm một khối if quanh định nghĩa để tránh xung đột vùng tên. Liệt kê 2 là một ví dụ về cách tiếp cận này.
    Liệt kê 2. Định nghĩa một cuộc gọi lại đã có tên trong cùng một khối mã
    function quoteWords()
    {
         if (!function_exists ('quoteWordsHelper')) {
             function quoteWordsHelper($string) {
                 return preg_replace('/(\w)/','"$1"',$string);
             }
          }
          return array_map('quoteWordsHelper', $text);
    }
  3. Chúng ta có thể sử dụng create_function(), là một bộ phận của PHP từ bản V4, để tạo ra hàm khi chạy. Về mặt chức năng, cho phép thực hiện điều chúng ta muốn, nhưng nó có một vài nhược điểm. Nhược điểm chính là nó được biên dịch lúc đang chạy so với thời gian biên dịch, điều mà sẽ không cho phép các bộ nhớ nhanh chứa mã thao tác (opcode cache) ẩn hàm đó. Đây là cú pháp khá tồi, và việc làm nổi bật chuỗi hiện nay trong phần lớn các IDE chỉ đơn giản là không làm việc.

Mặc dù các hàm mà chấp nhận các hàm gọi lại là mạnh mẽ, không có cách tốt nào để thực hiện một hàm gọi one-off (hàm được làm hoặc xảy ra chỉ một lần) mà không dùng đến việc thiếu đàng hoàng. Với PHP V5.3, chúng ta có thể sử dụng các hàm lambda để lập lại ví dụ trên bằng một cách trong sáng hơn.

Liệt kê 3. quoteWords() sử dụng một hàm lambda cho cuộc gọi lại
function quoteWords()
{
     return array_map('quoteWordsHelper',
            function ($string) {
                return preg_replace('/(\w)/','"$1"',$string);
            });
}

Chúng ta thấy cú pháp trong sáng cho phép định nghĩa các hàm này, có thể được tối ưu hóa về hiệu năng bằng các bộ nhớ mã thao tác. Chúng ta cũng đã tăng cường tính dễ đọc và tương thích đã được cải thiện với việc làm nổi bật chuỗi. Hãy xây dựng nên cái này để tìm hiểu về cách sử dụng các bao đóng trong PHP.


Các bao đóng

Các hàm lambda tự chúng không thêm nhiều thứ mà chúng ta trước đây không thể làm. Như đã thấy, chúng ta có thể làm toàn bộ việc này bằng cách sử dụng create_function(), mặc dù với cú pháp tồi hơn và hiệu năng thấp hơn lý tưởng. Tuy nhiên chúng vẫn là các hàm chỉ dùng một lần và không bảo trì bất cứ loại trạng thái mà hạn chế ta có thể làm với chúng. Đây là nơi các bao đóng bước vào và lấy các hàm lambda cho mức tiếp theo.

Bao đóng là một hàm mà được đánh giá về chính môi trường của nó, nó có một hoặc nhiều biến buộc (bound variables) mà có thể truy cập khi gọi hàm. Chúng đến từ giới lập trình hàm, nơi có một số khái niệm. Bao đóng giống như các hàm lambda, nhưng linh lợi hơn về khả năng tương tác với các biến từ ngoài môi trường xác định bao đóng.

Chúng ta hãy quan sát cách định nghĩa một bao đóng trong PHP. Liệt kê 4 cho một ví dụ về một bao đóng cho phép nhập một biến từ môi trường bên ngoài và chi đơn giản hiện chúng lên màn hình.

Liệt kê 4. Ví dụ bao đóng đơn giản
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };

$closure();

Output:
Hello World!

Các biến được nhập từ môi trường bên ngoài được quy định trong mệnh đề use của định nghĩa hàm bao đóng. Theo mặc định, chúng được chuyển theo giá trị, nghĩa là nếu chúng ta cập nhật giá trị đã chuyển trong định nghĩa hàm bao đóng, nó sẽ không cập nhật giá trị bên ngoài. Tuy nhiên chúng ta có thể thực hiện điều này bằng cách mào đầu biến này bằng toán tử &, được sử dụng trong các định nghĩa hàm để xác định cách chuyển tham số theo tham chiếu. Liệt kê 5 chỉ ra một ví dụ về việc này.

Liệt kê 5. Bao đóng chuyển các biến theo tham chiếu
$x = 1
$closure = function() use (&$x) { ++$x; }

echo $x . "\n";
$closure();
echo $x . "\n";
$closure();
echo $x . "\n";

Output:
1
2
3

Chúng ta nhìn thấy bao đóng sử dụng biến ngoài $x và gia tăng nó mỗi khi bao đóng được gọi. Chúng ta có thể trộn các biến chuyển theo giá trị với các biến chuyển theo tham chiếu một cách dễ dàng trong mệnh đề use, và chúng sẽ được xử lý mà không gặp phải bất kỳ vấn đề nào. Chúng ta cũng có thể có các hàm mà trực tiếp trả lại các bao đóng, như xem trong Liệt kê 6. Trong trường hợp này, tuổi thọ của bao đóng sẽ thực sự lâu hơn phương thức mà đã xác định chúng.

Liệt kê 6. Bao đóng được trả lại bởi một hàm
function getAppender($baseString)
{
      return function($appendString) use ($baseString) { return $baseString . 
$appendString; };
}

Bao đóng và các đối tượng

Bao đóng có thể là công cụ hữu ích không những cho lập trình thủ tục mà còn cho lập trình hướng đối tượng. Việc sử dụng các bao đóng có cùng một mục đích trong tình trạng này như nó sẽ có bên ngoài một lớp: để chứa một hàm riêng bị phụ thuộc trong một phạm vi nhỏ. Chúng cũng thực sự dễ sử dụng trong các đối tượng của chúng ta khi chúng ở bên ngoài một đối tượng.

Khi được xác định trong một đối tượng, có một điều tiện dụng là bao đóng có đủ quyền truy cập đến đối tượng qua biến $this không cần phải nhập nó tường minh. Liệt kê 7 giải thích việc này.

Liệt kê 7. Bao đóng bên trong một đối tượng
class Dog
{
    private $_name;
    protected $_color;

    public function __construct($name, $color)
    {
         $this->_name = $name;
         $this->_color = $color;
    }

    public function greet($greeting)
    {
         return function() use ($greeting) {
             echo "$greeting, I am a {$this->_color} dog named 
{$this->_name}.";
         };
    }
}

$dog = new Dog("Rover","red");
$dog->greet("Hello");

Output:
Hello, I am a red dog named Rover.

Ở đây, chúng ta rõ ràng là sử dụng câu chào được đưa cho phương thức greet() trong bao đóng được xác định trong nó. Chúng ta cũng nắm được màu sắc và tên của chú chó, chuyển qua hàm dựng và lưu lại trong đối tượng, trong bao đóng.

Các bao đóng xác định trong một lớp về cơ bản là giống như những thứ đã định nghĩa bên ngoài một đối tượng. Chỗ khác nhau duy nhất là việc nhập tự động đối tượng qua biến $this. Chúng ta có thể vô hiệu hóa hành vi này bằng cách định nghĩa bao đóng là tĩnh.

Liệt kê 8. Bao đóng tĩnh
class House
{
     public function paint($color)
     {
         return static function() use ($color) { echo "Painting the 
house $color...."; };
     }
}

$house = new House();
$house->paint('red');

Output:
Painting the house red....

Ví dụ này giống với lớp Dog được xác định trong liệt kê 5. Khác biệt lớn là ở chỗ chúng ta không sử dụng bất kỳ thuộc tính nào của đối tượng trong bao đóng, do nó được định nghĩa là tĩnh.

Cái lợi lớn của việc sử dụng một bao đóng tĩnh so với bao đóng không tĩnh (nonstatic) bên trong một đối tượng là để tiết kiệm bộ nhớ. Bằng cách không phải nhập đối tượng vào bao đóng, chúng ta có thể tiết kiệm khá nhiều bộ nhớ, đặc biệt là nếu chúng ta có nhiều bao đóng mà không cần tính năng này.

Một điều vui nữa cho các đối tượng là việc thêm vào một phương thức ma thuật tên là __invoke(), cho phép đối tượng tự gọi nó là một bao đóng. Nếu phương thức này được định nghĩa, nó sẽ được sử dụng khi đối tượng đó được gọi vào ngữ cảnh đó. Liệt kê 9 minh hoạ việc này.

Liệt kê 9. Sử dụng phương thức __invoke()
class Dog
{
    public function __invoke()
    {
         echo "I am a dog!";
    }
}

$dog = new Dog();
$dog();

Việc gọi tham chiếu đối tượng như trong Liệt kê 9 như biến tự động gọi phương thức ma thuật __invoke() làm cho lớp tự hành động như là một bao đóng.

Các bao đóng có thể tích hợp rất tốt với mã hướng đối tượng, cũng như là với mã thủ tục. Chúng ta hãy xem cách các bao đóng tương tác với API Phản chiếu (Reflection API) mạnh mẽ của PHP.


Bao đóng và phản chiếu

PHP có một API phản chiếu rất hữu ích, nó cho ta kỹ thuật đảo ngược các lớp (reverse-engineer), các giao diện, các hàm, và các phương thức. Theo thiết kế, các bao đóng là các hàm ẩn danh, có nghĩa là chúng không xuất hiện trong API phản chiếu.

Tuy nhiên, một phương thức mới getClosure() đã được thêm vào các lớp ReflectionMethodReflectionFunction trong PHP để tạo ra bao đóng một cách năng động từ hàm hoặc phương thức đã quy định. Nó hoạt động như một gộp lớn trong ngữ cảnh này, trong đó việc gọi ra phương thức của hàm thông qua bao đóng gây nên việc gọi hàm trong ngữ cảnh mà nó được định nghĩa. Liệt kê 10 cho biết cách công việc này thực hiện.

Liệt kê 10. Sử dụng phương thức getClosure()
class Counter
{
      private $x;

      public function __construct()
      {
           $this->x = 0;
      }

      public function increment()
      {
           $this->x++;
      }

      public function currentValue()
      {
           echo $this->x . "\n";
      }
}
$class = new ReflectionClass('Counter');
$method = $class->getMethod('currentValue');
$closure = $method->getClosure()
$closure();
$class->increment();
$closure();
Output:
0
1

Một hiệu quả phụ thú vị của cách tiếp cận này là ở chỗ nó cho phép chúng ta truy cập các thành viên riêng, được bảo vệ của một lớp, thông qua bao đóng mà có thể rất tiện cho các lớp kiểm thử môđun (unit testing). Liệt kê 11 là một ví dụ về việc truy cập một phương thức riêng trong một lớp.

Liệt kê 11. Truy cập một phương thức riêng trong một lớp
class Example 
{ 
     ....
     private static function secret() 
     { 
          echo "I'm an method that's hiding!"; 
     } 
     ...
} 

$class = new ReflectionClass('Example');
$method = $class->getMethod('secret');
$closure = $method->getClosure()
$closure();
Output:
I'm an method that's hiding!

Hơn nữa, chúng ta có thể sử dụng API phản chiếu để tự kiểm tra (introspect) một bao đóng, như trình bày trong Liệt kê 12. Chúng ta chỉ cần chuyển tham chiếu biến sang bao đóng vào hàm dựng của lớp ReflectionMethod.

Liệt kê 12. Kiểm tra một bao đóng bằng cách sử dụng sử dụng API phản chiếu
$closure = function ($x, $y = 1) {}; 
$m = new ReflectionMethod($closure); 
Reflection::export ($m);
Output:
Method [ <internal> public method __invoke ] {
  - Parameters [2] {
    Parameter #0 [ <required> $x ]
    Parameter #1 [ <optional> $y ]
  }
}

Một điều đáng lưu ý về tính tương thích ngược (backward-compatibility) là lớp có tên Closure bây giờ được công cụ PHP dành riêng để lưu giữ các bao đóng, như vậy bất kỳ lớp nào dùng tên đó sẽ cần phải đổi tên.

API phản chiếu hỗ trợ nhiều cho các bao đóng, như chúng đã thấy, ở dạng có thể tạo ra chúng từ các hàm và phương thức hiện hành một cách năng động. Chúng cũng có thể tự kiểm tra chính mình vào một bao đóng giống như một hàm bình thường.


Tại sao lại là bao đóng?

Như chúng ta đã thấy trong các ví dụ về các hàm lambda, một trong những cách sử dụng rõ nhất của các bao đóng là trong một vài hàm PHP mà chấp nhận một hàm gọi như tham số. Tuy nhiên, các bao đóng có thể hữu ích trong bất kỳ ngữ cảnh nào mà chúng ta cần phải đóng gói logic bên trong phạm vi của chính nó. Một ví dụ như vậy xảy ra khi tổ chức cải tiến lại (refactoring) mã cũ để giúp đơn giản hóa nó và làm cho nó có thể đọc hơn. Lấy ví dụ sau đây, nó cho thấy việc ghi log (logger) được sử dụng trong khi chạy một vài truy vấn SQL.

Liệt kê 13. Các truy vấn SQL ghi mã (Code logging SQL queries)
$db = mysqli_connect("server","user","pass"); 
Logger::log('debug','database','Connected to database'); 
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); 
Logger::log('debug','database','Insert Hammer into to parts table'); 
$db->query('insert into parts (part, description) values 
      ('Drill','Puts holes in wood');
Logger::log('debug','database','Insert Drill into to parts table'); 
$db->query('insert into parts (part, description) values ('Saw','Cuts wood'); 
Logger::log('debug','database','Insert Saw into to parts table');

Một điểm nổi bật trong Liệt kê 13 là chúng ta đang thực hiện bao nhiêu lần lặp. Mỗi cuộc gọi thực hiện đối với Logger::log() có cùng hai đối số đầu tiên. Để giải quyết việc này, chúng ta có thể đẩy phương thức đó sang một bao đóng và thay vào đó thực hiện các cuộc gọi đối lại bao đóng đó. Mã kết quả như dưới đây.

Liệt kê 14. Các truy vấn SQL ghi mã đã được tổ chức cải tiến lại
$logdb = function ($string) { Logger::log('debug','database',$string); };
$db = mysqli_connect("server","user","pass"); 
$logdb('Connected to database'); 
$db->query('insert into parts (part, description) values ('Hammer','Pounds nails'); 
$logdb('Insert Hammer into to parts table'); 
$db->query('insert into parts (part, description) values 
       ('Drill','Puts holes in wood');
$logdb('Insert Drill into to parts table'); 
$db->query('insert into parts (part, description) values ('Saw','Cuts wood'); 
$logdb('Insert Saw into to parts table');

Không những chúng ta đã khiến mã trong sáng hơn về bề ngoài, mà chúng ta còn làm cho nó dễ thay đổi mức độ ghi log của log truy vấn SQL, do chúng ta bây giờ chỉ cần thực hiện thay đổi ở một nơi.


Tóm tắt

Bài viết này đã chứng minh các bao đóng bổ ích như thế nào khi một việc lập trình hàm xây dựng bằng mã PHP V5.3. Chúng ta đã bàn luận về các hàm lambda và các ưu điểm mà các bao đóng đưa ra cho chúng. Các đối tượng và bao đóng hoà hợp với nhau rất tốt, như chúng ta đã thấy bằng cách xử lý đặc biệt các bao đóng trong mã hướng đối tượng. Chúng ta đã thấy được chúng ta có thể sử dụng API phản chiếu tốt như thế nào để tạo ra các bao đóng động, cũng như là tự kiểm tra chính mình các bao đóng hiện hành.

Tài nguyên

Học tập

Lấy sản phẩm và công nghệ

Thảo luận

Bình luận

developerWorks: Đăng nhập

Các trường được đánh dấu hoa thị là bắt buộc (*).


Bạn cần một ID của IBM?
Bạn quên định danh?


Bạn quên mật khẩu?
Đổi mật khẩu

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Ở lần bạn đăng nhập đầu tiên vào trang developerWorks, một hồ sơ cá nhân của bạn được tạo ra. Thông tin trong bản hồ sơ này (tên bạn, nước/vùng lãnh thổ, và tên cơ quan) sẽ được trưng ra cho mọi người và sẽ đi cùng các nội dung mà bạn đăng, trừ khi bạn chọn việc ẩn tên cơ quan của bạn. Bạn có thể cập nhật tài khoản trên trang IBM bất cứ khi nào.

Thông tin gửi đi được đảm bảo an toàn.

Chọn tên hiển thị của bạn



Lần đầu tiên bạn đăng nhập vào trang developerWorks, một bản trích ngang được tạo ra cho bạn, bạn cần phải chọn một tên để hiển thị. Tên hiển thị của bạn sẽ đi kèm theo các nội dung mà bạn đăng tải trên developerWorks.

Tên hiển thị cần có từ 3 đến 30 ký tự. Tên xuất hiện của bạn phải là duy nhất trên trang Cộng đồng developerWorks và vì lí do an ninh nó không phải là địa chỉ email của bạn.

Các trường được đánh dấu hoa thị là bắt buộc (*).

(Tên hiển thị cần có từ 3 đến 30 ký tự)

Bằng việc nhấn Gửi, bạn đã đồng ý với các điều khoản sử dụng developerWorks Điều khoản sử dụng.

 


Thông tin gửi đi được đảm bảo an toàn.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=70
Zone=Nguồn mở
ArticleID=444964
ArticleTitle=Có gì mới trong PHP 5.3, Phần 2: Bao đóng và các hàm lambda
publish-date=11062009