[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 | function each($items, $func) |
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 | function each($items, $func) |
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 | for ($i = 0; $i < count($productsToDelete); $i++) { |
…Và viết lại nó như sau, rõ ràng hơn một chút
1 | each($productsToDelete, function ($product) { |
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ân1
2
3each($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 | function map($items, $func) |
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
3map($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 $1001
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
19function 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ảimap
các sản phẩm với giá, nhưng bạn luôn luônfilter
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
18function 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 map
và filter
trong điều kiện của reduce
1
2
3
4
5
6
7function map($items, $func)
{
return reduce($items, function ($mapped, $item) use ($func) {
$mapped[] = $func($item);
return $mapped;
}, []);
}
Note 2:
Vì
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ênreduce
)
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
10function 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 list1
2
3
4
5
6
7
8
9
10function 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 =))
[Collection] Functional Building Blocks
http://yoursite.com/2019/12/27/Collection-Functional-Building-Blocks/