12/08/2018, 15:59

Functional pattern: flatMap

Trong bài, chúng ta xem xét cách mà flatMap hoạt động, tương tự như map() trong Array, nhưng linh hoạt hơn. Cả hai map () vàflatMap () đều lấy một hàm f làm tham số để control cách dịch một mảng đầu vào sang một mảng đầu ra: Với map (), mỗi phần tử của mảng đầu vào được dịch chính xác sang ...

Trong bài, chúng ta xem xét cách mà flatMap hoạt động, tương tự như map() trong Array, nhưng linh hoạt hơn.

Cả hai map () vàflatMap () đều lấy một hàm f làm tham số để control cách dịch một mảng đầu vào sang một mảng đầu ra:

  • Với map (), mỗi phần tử của mảng đầu vào được dịch chính xác sang một phần tử ở mảng đầu ra. Nghĩa là, f trả về một giá trị duy nhất.
  • Với flatMap (), mỗi phần tử của mảng đầu vào được dịch sang 0 hoặc nhiều phần tử ở mảng đầu ra. Nghĩa là, f trả về một mảng các giá trị. Đây là cách một flatMap () thực hiện:
function flatMap(arr, mapFunc) {
    const result = [];
    for (const [index, elem] of arr.entries()) {
        const x = mapFunc(elem, index, arr);
        // We allow mapFunc() to return non-Arrays
        if (Array.isArray(x)) {
            result.push(...x);
        } else {
            result.push(x);
        }
    }
    return result;
}

flatMap () đơn giản hơn nếu mapFunc () chỉ được phép trả lại Array, nhưng chúng ta không đặt sự hạn chế này ở đây, bởi vì các giá trị non-Array đôi khi có ích (Xem the section on flatten() như một ví dụ). Để minh hoạ cách flatMap () hoạt động, chúng ta sử dụng hàm fillarray:

function fillArray(x) {
    return new Array(x).fill(x);
}

Đây là cách hàm fillArray() hoạt động:

> fillArray(1)
[ 1 ]
> fillArray(2)
[ 2, 2 ]
> fillArray(3)
[ 3, 3, 3 ]

Đây là cách flatMap() hoạt động:

> flatMap([1,2,3], fillArray)
[ 1, 2, 2, 3, 3, 3 ]

Kết quả của Array method map () luôn luôn có cùng độ dài với Array mà nó được gọi vào. Tức là, callback của nó không thể bỏ qua các phần tử Array mà nó không quan tâm. Khả năng của flatMap () để làm như vậy là hữu ích trong ví dụ tiếp theo: processArray () trả về một Array, trong đó mỗi phần tử là một giá trị bọc hoặc một lỗi bọc.

function processArray(arr, process) {
    return arr.map(x => {
        try {
            return { value: process(x) };
        } catch (e) {
            return { error: e };
        }
    });
}
const results = processArray(myArray, myFunc);

FlatMap () cho phép chúng ta trích xuất các giá trị hoặc chỉ là các lỗi từ kết quả:

const values = flatMap(results,
    result => result.value ? [result.value] : []);
const errors = flatMap(results,
    result => result.error ? [result.error] : []);

map () ánh xạ từng phần tử mảng đầu vào thành một phần tử mảng đầu ra. Nhưng nếu chúng ta muốn map nó cho nhiều phần tử đầu ra? Cùng xem ví dụ sau: Component (React) TagList được gọi với hai thuộc tính.

<TagList tags={['foo', 'bar', 'baz']}
             handleClick={x => console.log(x)} />

Attributes:

  • Một mảng các tags, mỗi tag là một chuỗi.
  • Một callback để xử lý click trên thẻ. TagList được hiển thị dưới dạng một chuỗi các liên kết và được phân cách bằng dấu phẩy:
class TagList extends React.Component {
    render() {
        const {tags, handleClick} = this.props;
        return flatMap(tags,
            (tag, index) => [
                ...(index > 0 ? [', '] : []), // (A)
                <a key={index} href=""
                   onClick={e => handleClick(tag, e)}>
                   {tag}
                </a>,
            ]);
    }
}

Ở dòng A, chúng ta đang chèn điều kiện phần tử ',' thông qua toán tử operator (...). Thủ thuật này được giải thích tại đây.

Do flatMap () mà TagList được hiển thị như một mảng đơn. Tag đầu tiên đóng góp một phần tử cho mảng này (một link); Mỗi tag còn lại đóng góp hai phần tử (dấu phẩy và link).

Arbitrary iterables

flatMap () có thể được tổng quát để làm việc với các iterables tùy ý:

function* flatMapIter(iterable, mapFunc) {
    let index = 0;
    for (const x of iterable) {
        yield* mapFunc(x, index);
        index++;
    }
}

Hàm flatMapIter () hoạt động với Mảng:

function fillArray(x) {
    return new Array(x).fill(x);
}
console.log([...flatMapIter([1,2,3], fillArray)]); // (A)
    // [1, 2, 2, 3, 3, 3]

Ở dòng A, chúng ta dịch các iterable trả về bởi flatMapIter () vào một Array, thông qua toán tử operator (...). Một đặc điểm tốt của flatMapIter () là nó hoạt động tăng dần: ngay khi giá trị đầu vào đầu tiên sẵn sàng, đầu ra sẽ được tạo ra. Ngược lại, flatMap() cần tất cả đầu vào của nó để tạo ra đầu ra của nó. Điều đó có thể được chứng minh thông qua vòng lặp vô hạn tạo ra bởi function naturalNumbers():

function* naturalNumbers() {
    for (let n=0;; n++) {
        yield n;
    }
}
const infiniteInput = naturalNumbers();
const infiniteOutput = flatMapIter(infiniteInput, fillArray);
const [a,b,c,d,e] = infiniteOutput; // (A)
console.log(a,b,c,d,e);
    // 1 2 2 3 3

Ở dòng A, chúng ta trích ra 5 giá trị đầu tiên của infiniteOutput thông qua destructuring.

Implementing flatMap() via reduce()

Bạn có thể sử dụng phương thức reduce () để thực hiện một phiên bản đơn giản của flatMap ():

function flatMap(arr, mapFunc) {
    return arr.reduce(
        (prev, x) => prev.concat(mapFunc(x)),
        []
    );
}

flatten () là một thao tác nối tất cả các phần tử của một Array:

> flatten(['a', ['b','c'], ['d']])
[ 'a', 'b', 'c', 'd' ]

Nó có thể được thực hiện như sau: const flatten = (arr) => [].concat(...arr) Sử dụng map () và flatteningg kết quả cũng giống như sử dụng flatMap (). Hai biểu thức sau đây là tương đương:

flatten(arr.map(func))
flatMap(arr, func)

Tương tự, sử dụng flatMap ()vớiidentity function (x => x) cũng giống như sử dụng flatten (). Hai biểu thức sau đây là tương đương:

flatMap(arr, x => x)
flatten(arr)

Conditionally inserting values into an Array

Code sau chỉ chèn 'a' nếu cond là true:

const cond = false;
const arr = flatten([
  (cond ? ['a'] : []),
  'b',
]);
    // ['b']

Đọc post sau để có thêm thông tin “Conditionally adding entries inside Array and object literals”.

Filtering out failures

Trong ví dụ sau, downloadFiles () chỉ trả về các văn bản có thể được tải xuống.

async function downloadFiles(urls) {
    const downloadAttempts = await Promises.all( // (A)
        urls.map(url => downloadFile(url)));
    return flatten(downloadAttempts); // (B)
}
async function downloadFile(url) {
    try {
        const response = await fetch(url);
        const text = await response.text();
        return [text]; // (C)
    } catch (err) {
        return []; // (D)
    }
}

downloadFiles() đầu tiên maps từng URL cho một Promise giải quyết:

  • Một Array với văn bản đã được tải xuống thành công (dòng C)
  • Một mảng rỗng (dòng D) Promises.all () (dòng A) chuyển đổi Array of Promises thành Promise để giải quyết một mảng nhiều chiều. await (dòng A) unwraps rằng Promise và flatten () un-nests mảng (dòng B). Lưu ý rằng chúng ta không thể sử dụng flatMap () ở đây, bởi vì rào cản được áp đặt bởi các Promises trả về bởi downloadFile (): khi nó trả về một giá trị, nó không biết liệu nó sẽ là một văn bản hay một mảng rỗng.
  • “A collection of Scala ‘flatMap’ examples” của Alvin Alexander
  • Sect. “Transformation Methods” (which include map()) trong “Speaking JavaScript”.
  • Conditionally adding entries inside Array and object literals

http://2ality.com/2017/04/flatmap.html

0