[Collection] Functional Building Blocks

Map chỉ là 1 trong hàng chục hàm bậc cao mạnh (HOF) mẽ để làm việc với mảng. Chúng ta sẽ nói về rất nhiều về chúng trong các ví dụ sau, nhưng chúng ta hãy xem xét 1 số điều cơ bản ở những cấp độ đầu tiên.

EACH

Each is no more than a foreach loop wrapped inside of a higher order function: (không khác gì 1 vòng lặp foreach)

1
2
3
4
5
6
function each($items, $func)
{
foreach ($items as $item) {
$func($item);
}
}

Bạn có thể tự hỏi là “tại sao ai đó lại quan tâm đến việc này”? Một trong những lợi ích là nó ẩn các chi tiết thực hiện của vòng lặp (và chúng tôi ghét vòng lặp) :D

Hãy tưởng tượng PHP không có vòng lặp foreach. Khi đó để thực thi each, chúng ta sẽ làm như sau:

1
2
3
4
5
6
function each($items, $func)
{
for ($i = 0; $i < count($items); $i++) {
$func($items[$i]);
}
}

Và với cách này, có 1 sự trừu tượng cần thực hiện là “làm điều này với mọi phần tử trong mảng”. Nó sẽ như sau:

1
2
3
for ($i = 0; $i < count($productsToDelete); $i++) {
$productsToDelete[$i]->delete();
}

…Và viết lại nó như sau, rõ ràng hơn một chút

1
2
3
each($productsToDelete, function ($product) {
$product->delete();
});

Each cũng trở nên cải tiến rõ ràng so với việc sử dụng foreach trực tiếp ngay cả khi bạn nhận nó trong toán tử của hàm nào đó, cái mà chúng ta sẽ đề cập trong phần sau của cuốn sách.

Một vài thứ bạn cần nhớ với each:

  • Nếu bạn đang muốn sử dụng một loại biến nào đó để thu thập giá trị (1 biến lưu giá trị) (sử dụng 1 biến để lưu giá trị của collection), each không phải là một sự lựa chọn.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // Bad! Use `map` instead.
    each($customers, function ($customer) use (&$emails) {
    $emails[] = $customer->email;
    });

    // Good!
    $emails = map($customers, function ($customer) {
    return $customer->email;
    });
  • Không giống như các toán tử cơ bản với mảng khác, each không trả về giá trị. Điều nó gợi ý cho bạn rằng nó nên để dành cho thực hiện hành đồng, như xóa products, shipping orders, sending emails, vân vân
    1
    2
    3
    each($orders, function ($order) {
    $order->markAsShipped();
    });

MAP

Map được sử dụng để transform (chuyển đổi) mỗi item trong mảng sang cái gì đó. Nhận vào 1 vài mảng của các items và 1 hàm, map sẽ apply hàm đó với mỗi phần tử và tạo ra mảng mới giống size

1
2
3
4
5
6
7
8
9
10
function map($items, $func)
{
$result = [];

foreach ($items as $item) {
$result[] = $func($item);
}

return $result;
}

Nhớ rằng mỗi phần tử của mảng mới có quan hệ tương ứng phần tử mảng gốc. Một cách tốt để nhớ map làm việc như thế nào là nghĩ nó là 1 mapping giữa mỗi phần tử mảng cũ và mảng mới.

Map sẽ công cụ tuyệt vời để thực hiện:

  • Lấy, trích xuất 1 trường từ 1 mảng các đối tượng, như là mapping customes vào địa chỉ email:
    1
    2
    3
    $emails = map($customers, function ($customer) {
    return $customer->email;
    });
  • Tạo một mảng các đối tượng từ dữ liệu thô, như mapping 1 mảng kết quả dữ liệu JSON vào mảng đối tượng ta quan tâm:
    1
    2
    3
    $products = map($productJson, function ($productData) {
    return new Product($productData);
    });
  • Chuyển đổi mỗi phần tử với 1 format mới:
    1
    2
    3
    $displayPrices = map($prices, function ($price) {
    return '$' . number_format($price / 100, 2);
    });

MAP vs EACH

Một lỗi chung tôi nhìn thấy của mọi người là sử dụng map khi mà họ nên sử dụng each.

Xem xét ví dụ each sau khi xóa products. Bạn có thể thực thi sử dụng mao và về mặt kỹ thuật thì nó không hề ảnh hưởng:

1
2
3
map($productsToDelete, function ($product) {
$product->delete();
});

Mặc dù code hoạt động nhưng dường như nó chưa đúng về mặt ngữ nghĩa. Chúng ta đang không map cái qué gì cả. Đoạn code sẽ đi qua tất cả các lỗi (kiểu các lỗi có thể gặp phải) tạo 1 mảng mới cho chúng ta và mảng được tạo với mỗi phần tử là null và chúng ta không làm gì với chúng.

Map được dùng để transforming 1 mảng vào mảng mới. Nếu bạn không chuyển đổi cái gì, bạn không nên dùng map.

Như 1 chuẩn chung, bạn nên sử dụng each thay cho map nếu 1 trong nhưng điều sao là đúng:

  • Callback không trả về gì?
  • map có trả về dữ liệu nhưng về sau bạn lại không thực hiện thao tác gì với giá trị này (You don’t do anything with the return value of map).
  • Bạn chỉ đang cố gắng thực hiện 1 vài hành động gì đó với mỗi phẩn tử của mảng.

FILTER

Chúng ta có 1 danh sách các sản phẩm và chúng ta biết cái nào đã hết. Nếu không sử dụng HOF, code sẽ như sau:

1
2
3
4
5
6
7
8
9
$outOfStockProducts = [];

foreach ($products as $product) {
if ($product->isOutOfStock()) {
$outOfStockProducts[] = $product;
}
}

return $outOfStockProducts;

Tương tự, nếu bạn muốn products cái mà có giá lớn hơn $100
1
2
3
4
5
6
7
8
9
$expensiveProducts = [];

foreach ($products as $product) {
if ($product->price > 100) {
$expensiveProducts[] = $product;
}
}

return $expensiveProducts;

Điểm khác biệt giữa 2 ví dụ trên chỉ là điều kiện và ta có thể trừu tượng hóa nó tương tự map. Hàm mà chúng ta để thực hiện việc đó được gọi là filter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function filter($items, $func)
{
$result = [];

foreach ($items as $item) {
if ($func($item)) {
$result[] = $item;
}
}

return $result;
}

$outOfStockProducts = filter($products, function ($product) {
return $product->isOutOfStock();
});
$expensiveProducts = filter($products, function ($product) {
return $product->price > 100;
});

Toán tử filter được sử dụng để filter out (loại bỏ khỏi) 1 vài phần tử ra khỏi mảng mà bạn không muốn. Bạn nói với filter rằng phần tử nào truyền callback vào trả về true nếu bạn muốn giữ nó, trả về false nếu bạn muốn xóa nó.

Một thứ quan trọng để hiểu về filter là nó KHÔNG thay đổi hay chuyển đổi giá trị trong mảng, nó chỉ đẩy các phần tử bạn không muốn. Điều đó có nghĩa các phần tử trong mảng mới không chỉ giống kiểu với các phần tử mảng cũ, chúng là same items.

Điều này thì hoàn toàn trái ngược với map. Bạn phải map các sản phẩm với giá, nhưng bạn luôn luôn filter các sản phẩm vào mảng sản phẩm mới

Reject: ngược với filter

REDUCE

Chúng ta có giỏ hàng mua sắm của các items và chúng ta cần tính toán tổng giá của giỏ hàng đó. Một cách đơn giản chúng ta làm là lặp qua tất cả các phần tử và tính toán:

1
2
3
4
5
6
7
$totalPrice = 0;

foreach ($cart->items as $item) {
$totalPrice = $totalPrice + $item->price;
}

return $totalPrice;

Bây giờ hãy tưởng tượng vấn đề khác nơi bạn muốn gửi 1 email tới nhóm khách hàng và bạn cần sinh ra BCC line là dấu phẩy phân cách các email đó:
1
2
3
4
5
6
7
$bcc = '';

foreach ($customers as $customer) {
$bcc = $bcc . $customer->email . ', ';
}

return $bcc;

Câu hỏi được đặt ra tương tự, liệu có thể trừu tượng hóa toán tử này?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function reduce($items, $callback, $initial)
{
$accumulator = $initial;

foreach ($items as $item) {
$accumulator = $callback($accumulator, $item);
}

return $accumulator;
}

$totalPrice = reduce($cart->items, function ($totalPrice, $item) {
return $totalPrice + $item->price;
}, 0);

$bcc = reduce($customers, function ($bcc, $customer) {
return $bcc . $customer->email . ', ';
}, '');

Higher order function như trên được gọi là reduce, cái mà nhận vào tất cả các biến chúng ta muốn lấy như 1 tham số.

With Great Power

Note 1:

Toán tử reduce được sử dụng để nhận vào mảng các items và cắt giảm (reduce) nó xuống thành 1 giá trị đơn. Không có ràng buộc gì về giá trị đơn này, nó có thể là 1 số, 1 chuỗi, 1 object hoặc thậm chí là bất cứ gì bạn muốn, không vấn đề.

Nó thậm chí có thể được sử dụng để giảm 1 mảng items vào 1 mảng khác, điều đó có nghĩa bạn thậm chí có thể thực thi mapfilter trong điều kiện của reduce

1
2
3
4
5
6
7
function map($items, $func)
{
return reduce($items, function ($mapped, $item) use ($func) {
$mapped[] = $func($item);
return $mapped;
}, []);
}

Note 2:

reduce là toán tử hàm ở level khá thấp, cái có thể biến 1 mảng thành bất cứ cái gì, và nó không phải luôn luôn giàu tính biểu đạt @@. Thỉnh thoảng khi bạn tự tìm thấy cách sử dụng reduce, cái mà bạn thực sự muốn là 1 higher level abstraction được built trên cao hơn của reduce, liên kết cái bạn đang cố làm rõ (kiểu sử dụng reduce ở mức thấp để trừu tượng hóa ở dưới, cái mình cần là lớp cao hơn, xây dựng các hàm giàu sức biểu đạt hơn như sum được xây dựng dựa trên reduce)

Ví dụ tính tổng giá, ta tạo một trừu tượng mới trên top của reduce với tên gọi là sum:

1
2
3
4
5
6
7
8
9
10
function sum($items, $callback)
{
return reduce($items, function ($total, $item) use ($callback) {
return $total + $callback($item);
}, 0);
}

$totalPrice = sum($cart->items, function ($item) {
return $item->price;
});

And BCC list
1
2
3
4
5
6
7
8
9
10
function join($items, $callback)
{
return reduce($items, function ($string, $item) use ($callback) {
return $string . $callback($item);
}, '');
}

$bcc = join($customers, function ($customer) {
return $customer->email . ', ';
});

Trong 4 toán tử chúng ta tìm hiểu, reduce chắc chắn là khó nhớ nhất để áp dụng cho bạn. Khi thực hành, hãy cố gắng thực thi lại filter với reducer là và xem những gì bạn đã nghĩ ra =))

Author

Ming

Posted on

2019-12-27

Updated on

2024-08-08

Licensed under

Comments