12/08/2018, 14:35

Write no "for" loop

Bạn chắc là đã rất quen với vòng lặp rồi. Bạn thấy vòng lặp hoàn toàn dễ hiểu vì đó là một trong những cái bạn học đầu tiên khi bắt đầu học lập trình. Ai mà chả đọc được vòng lặp. Chẳng có lý do gì để không dùng nó cả. Nhưng nếu mình bảo có cách viết khác dễ đọc hơn vòng lặp thì sao. Cơ mà dù thế ...

Bạn chắc là đã rất quen với vòng lặp rồi. Bạn thấy vòng lặp hoàn toàn dễ hiểu vì đó là một trong những cái bạn học đầu tiên khi bắt đầu học lập trình. Ai mà chả đọc được vòng lặp. Chẳng có lý do gì để không dùng nó cả. Nhưng nếu mình bảo có cách viết khác dễ đọc hơn vòng lặp thì sao. Cơ mà dù thế thì cũng chưa chắc nó dễ viết hay dễ đọc hơn vì dù sao thì chúng ta đã quen với vòng lặp từ rất lâu rồi. Thế nên hãy bắt đầu với vài lý do mà mình nghĩ là nhược điểm của vòng lặp.

Why not

Đầu tiên là mutable state. Để dùng vòng lặp for, hầu hết trường hợp bạn phải sử dụng một biến ở bên ngoài và thay đổi nó qua từng vòng lặp. Bạn cũng biết tại sao global state lại không tốt đúng không. Đó là vì nó được dùng ở khắp nơi và cũng có thể bị thay đổi ở khắp nơi nên không thể biết chắc giá trị của nó mỗi khi muốn dùng đến. Nhưng mà không phải local state mà có thể bị thay đổi thì cũng chẳng khác gì sao. Nếu bạn đang viết code JavaScript thì chắc bạn cũng biết một ví dụ hay được nhắc đến về lỗi khi sử dụng Closure. Bạn có thể đọc nó ở đây. Nếu lần đầu bị dính cái lỗi đó có khi bạn nghĩ đó là tại JavaScript ngu. Nhưng mà đấy không phải là ví dụ hoàn hảo cho tình huống đau đầu có thể xảy ra với mutable state đó sao.

Tiếp theo là một chỉ số gọi là cyclomatic complexity. Nếu bạn đã từng dùng một tool check code quality thì chắc bạn cũng đã từng thấy nó rồi. Để cho đơn giản thì đó là số điều kiện (if, else), vòng lặp (for, while) trong code của bạn. Con số này càng cao thì khả năng có lỗi càng cao và code càng khó đọc. Và tool check quality sẽ complain về cái này, điểm cho code sẽ bị giảm đi (Nếu bạn là một người thích sự hoàn hảo thì cái này mới là vấn đề chính, đúng không, đúng không! =_=).

Vậy hãy cùng thử xem bạn có thể thay thế và chia tay với vòng lặp for như thế nào.

Trong bài này mình viết các ví dụ bằng JavaScript. Tuy nhiên, bạn hoàn toàn có thể áp dụng với các ngôn ngữ khác. Một số có thể có hạn chế đôi chút như với PHP thì bạn không thể chain method với array được chẳng hạn. Hay Java thì từ Java 8 bạn mới có thể dùng stream để chain method với filter, map .etc được.

Cho đến ES2015 thì class Array của JavaScript đã có đầy đủ các hàm map, reduce, filter, first .etc rồi thế nên trong bài này mình sẽ dùng các hàm này của JavaScript. Các thư viện thông dụng như lodash, ramda, lazy.js hay cả jQuery cũng có đầy đủ các hàm này với tên tương tự nên các bạn cũng có thể sử dụng.

Trước hết hãy thử xem qua các cấu trúc vòng lặp phổ biến mà bạn hay viết.

Each

Đầu tiên là khi bạn cần làm gì đó với từng phần tử trong array.

for (let i in a) {
    let n = a[i];
    doSomething(n);
}

Đây chắc là là trường hợp mà bạn sử dụng vòng lặp thường xuyên nhất. Bạn có thể viết lại thế này với hàm forEach của array hay each nếu bạn đang dùng một thư viện khác.

a.forEach((item, index) => {
    doSomething(item);
});

Map

Một trường hợp khác cũng gần giống cái trên và cũng được dùng rất thường xuyên đó là tạo một array mới từ các phần tử của array ban đầu.

let b = [];

for (let i in a) {
    let newValue = doSomething(a[i]);
    b.push(newValue);
}

Đây là lúc bạn dùng đến hàm map. Hàm này sẽ tạo một array mới với các phần tử là các kết quả của hàm được truyền vào với mỗi phần tử của array cũ.

let b = a.map((item, index) => doSomething(item))

Reduce

Cuối cùng là khi bạn cần tính một giá trị mới từ các giá trị trong array

let total = 0;

for (let i in a) {
    total += a[i];
}

Trường hợp này thì bạn có thể dùng hàm reduce. Hàm này hơi phức tạp hơn 2 cái trước. Bạn có thể đọc document ở đây. Đoạn for ở trên có thể viết lại với hàm reduce thế này.

let b = a.reduce((total, value, index) => value + item, 0)

Vừa rồi là mấy trường hợp mình nghĩ là thông dụng nhất khi bạn phải dùng đến vòng lặp. map và reduce là 2 hàm mình thường dùng đến nhất. Mấy trường hợp trên chắc đã chiếm gần hết các vòng lặp mà bạn phải viết rồi. Tuy nhiên vòng lặp mà bạn hay phải viết thường có nội dung bên trong phức tạp hơn nhiều. Nếu chỉ dùng có mấy hàm như trên thì chỉ như kiểu viết lại cho đẹp hơn thôi chứ cũng chưa giúp ích nhiều lắm để khiến code của bạn dễ đọc hơn. Thế nên chúng ta hãy tiếp tục thêm một vài ví dụ khác nhé.

Filter

Khi dùng vòng lặp bạn cũng rất hay phải dùng đến if bên trong. Chúng ta hãy xét ví dụ này.

let total = 0;

for (let value of a) {
    if (n > 0) {
        total += value;
    }
}

Bạn có thể dùng filter cho trường hợp này. Nó sẽ trả về một array với các phần tử thỏa mãn điều kiện. Đoạn code trên sẽ được viết lại thế này.

let total = a.filter(value => value > 0)
             .reduce((total, value) => total + value, 0)

Đây là một ví dụ cho reduce, nhưng mà kết quả trả về của nó cũng là một array nên bạn hoàn toàn có thể sử dụng tương tự cho các hàm khác nhé.

Map & reduce

Thường thì bạn có thể phải viết nhiều hơn 1 cái if trong vòng lặp. Có khi có cả else nữa. Bạn có thể dùng map và reduce cho mấy trường hợp này.

Ví dụ vòng lặp thế này

let total = 0;

for (let value of a) {
    if (n > 0) {
        total += value;
    } else {
        total += value * -1;
    }

    doSomethingMore(value);
}

Thì bạn có thể viết thế này

let total = a.map(value => value > 0 ? value : value * -1 )
             .reduce((total, value) => total + value, 0)
             .forEach(value => doSomethingMore(value))

Group by

Tiếc là JavaScript không có hàm này cho Array. Nhưng mà mình nghĩ cũng có lúc phải dùng đến. Các thư viện như lodash, ramda, lazy.js cũng đều có hàm này. Thế nên hãy cùng xem một ví dụ.

let total = 0;

for (let value of a) {
    if (n > 0) {
        doSomething(value);
    } else {
        doSomeOtherThing(value);
    }
}

Dùng groupBy:

let {positive, negative} = _.groupBy(a, (value) => a > 0 ? 'positive' : 'negative');
positive.forEach(value => doSomething(value));
negative.forEach(value => doSomeOtherThing(value));

Reverse

Nếu bạn muốn vòng lặp chạy từ cuối array thay vì từ đầu thì sao. Đơn giản, chỉ cần đảo ngược array lại thôi.

let b = [];

for (let i = a.length - 1; i >= 0; i--) {
    let newValue = doSomething(a[i]);
    b.push(newValue);
}

Viết lại thành thế này

let b = a.reverse()
         .map((item) => doSomething(item));

Ngoài ra nếu dùng reduce thì bạn có thể dùng hàm reduceRight nữa. Cách dùng thì giống hệt reduce chỉ có điều nó sẽ bắt đầu từ cuối array thay vì từ đầu thôi.

Break, Continue

goto

Nếu bạn đã biết đến câu lệnh goto thì chắc các bạn cũng đã biết tại sao nó không tốt và bạn thật sự không nên dùng nó. Còn nếu bạn chưa biết đến nó thì bây giờ chắc bạn cũng biết tại sao chẳng còn ai dùng nó nữa rồi. Bởi vì bạn thật sự không nên dùng đến nó. Nhưng nếu nghĩ lại lần nữa, liệu có phải thế không nhỉ. Không phải break cũng chính là goto trá hình đó sao. Cùng xem ví dụ này nhé.

for (let value of a) {
    if (value > 0) {
        doSomething(value);
        break;
    }
}

Đó chẳng phải là goto doSomething đó sao. Thật ra thì mình thấy nó cũng không hẳn tệ lắm. Nhưng mà chúng ta có cách viết khác dễ đọc hơn nhiều. Trong phần này chúng ta hãy cùng xét các trường hợp mà mình nghĩ là ít phổ biến hơn cả, khi bạn phải dùng đến break hay continue.

Đầu tiên hãy xét ví dụ ở trên trước. Chúng ta sẽ phải doSomething với giá trị đầu tiên trong array thỏa mãn điều kiện. Chúng ta sẽ dùng hàm find để tìm phần tử đầu tiên này. Nó tương tự như filter chỉ khác là nó chỉ trả về phần tử đầu tiên thôi. Bạn sẽ viết lại nó thế này.

let number = a.find((value) => value > 0);
doSomething(value);

Chúng ta cùng xét tiếp một ví dụ tương tự.

let total = 0;

for (let value of a) {
    total += value;
    if (value > 0) {
        break;
    }
}

Ví dụ này cũng gần giống cái trước. Chúng ta sẽ doSomething cho đến khi gặp một giá trị thỏa mãn điều kiện. Trong trường hợp này chúng ta sẽ lấy tất cả phần tử từ đầu đến giá trị thỏa mãn điều kiện rồi map hoặc reduce nó. Để làm được vậy thì bạn sẽ cần dùng đến findIndex và slice. findIndex thì cũng giống như find, chỉ khác là nó sẽ trả về index của phần tử. slice thì sẽ trả về một phần của array.

Bạn sẽ viết lại đoạn code trên thế này

let untilFirstPositive = a.slice(0, a.findIndex(value => value > 0));
let total = untilFirstPositive.reduce((total, value) => total + value, 0);

Các thư viện như lodash cũng có thêm hàm takeWhile và cả takeWhileRight để bạn dùng trong trường hợp này cho tiện.

let untilFirstPositive = _.takeWhile(value => value < 0);
let total = untilFirstPositive.reduce((total, value) => total + value, 0);

Vừa nãy là 2 ví dụ về break rồi. Hãy cùng xem nốt 1 ví dụ cho continue. Cách dùng phổ biến cho continue là để bỏ qua một giá trị trong vòng lặp. Mình thấy nó cũng giống như if và else thôi nhỉ. Vậy nên mình sẽ dùng filter hoặc groupBy như trước thôi. Ví dụ thế này.

let total = 0;

for (let value of a) {
    if (value > 0) {
        continue;
    }

    total += value;
}

Sẽ viết được thành thế này

let total = a.filter(value => value > 0)
             .reduce((total, value) => total + value, 0);

Repeat

Còn một trường hợp nho nhỏ cuối cùng nữa. Đó là khi bạn cần làm một cái gì đó lặp lại n lần, không liên quan gì đến array cả. Mình thấy cũng ít khi phải dùng đến cái này. JavaScript cũng không có hàm nào để làm cái này nữa. Đây cũng chẳng phải trường hợp mà không dùng for thì sẽ đem lại được lợi ích gì cả. Tuy nhiên vì nhân tiện trong bài này, và nếu bạn muốn chia tay hẳn với for thì cũng có đôi cách để làm cái này.

Ví dụ mình có vòng lặp thế này

for (let i = 0; i < 10; i++) {
    drawCat();
}

Cách dễ: dùng hàm _.times trong lodash (ramda, lazy.js cũng có).

_.times(10, drawCat);

Cách khác: dùng recursive function.

const drawCatRec = (times) => {
    if (times > 0) {
        drawCat();
        drawCatRec(times - 1);
    }
}

drawCatRec(10);

Cách này không đơn giản như cách trước hơn nữa nó lại còn là recursive function nữa. Tuy nhiên bạn có thể thấy cái recursive function ở trên thực ra giống một vòng lặp while và có thể thay thế luôn while nếu bạn thích.

Bye

Vừa rồi là các trường hợp mà mình có thể nghĩ ra để dùng đến vòng lặp. Mình thấy viết code với map và reduce sẽ trông dễ nhìn hơn nhiều so với dùng vòng lặp. Hơn nữa còn tránh phải dùng đến mutable state. Ngoài ra chắc bạn cũng thấy có vài điểm đáng để ý. Đầu tiên các hàm được dùng ở trên đều tạo một array mới thay vì thay đổi cái cũ như bạn vẫn làm với vòng lặp for. Tạo nhiều array như vậy có vẻ không hiệu quả lắm, nhưng mà cái đấy xin để dịp khác bàn tới. Thứ hai là mỗi hàm đó thực ra là một vòng lặp. Nếu bạn chain 5, 6 cái liền thì thực ra là đang viết 5, 6 cái vòng lặp thay vì 1 vòng lặp nếu bạn viết bằng for. Nghe cũng không vui tí nào. Và đó cũng là lí do mà lazy.js trở nên đặc biệt so với các thư viện khác. Cái đó cũng xin để dịp khác bàn tới. Bye!

0