Bài viết được sự cho phép của tác giả Tống Xuân Hoài
Vấn đề
Khái niệm Functor là một bước đệm để từ đó giúp cho bạn khám phá ra những điều mới mẻ trong thế giới lập trình hàm. Vậy thì functor là gì và nó mang lại lợi ích gì trong lập trình?
Functor là gì?
Về bản chất, functor là một cấu trúc dữ liệu mà bạn có thể map
qua chúng để áp dụng một hàm vào từng phần tử với mục đích sửa đổi dữ liệu. Nhưng một điều quan trọng là dữ liệu đó được chứa trong một “vùng chứa”, để có thể sửa được giá trị thì các hàm phải lấy ra, sửa đổi rồi đặt giá trị vào “vùng chứa”.
Functor hay còn được kí hiệu là fmap. Đây là định nghĩa chung của fmap:
fmap :: (A -> B) -> Wrapper(A) -> Wrapper(B)
Hàm fmap nhận một hàm (A -> B) biến đổi hàm Wrapper(A) thành Wrapper(B) sau khi đã thực hiện việc biến đổi các giá trị A thành B. Để hiểu rõ hơn bạn có thể xem hình dưới:
Chúng ta thấy giá trị 1 được lấy ra khỏi “vùng chứa” -> áp dụng hàm -> đặt lại vào “vùng chứa”.
Về cơ bản fmap sẽ trả về một bản sao mới của “vùng chứa” tại mỗi lần gọi nên nó có thể coi là bất biến.
Đó là lý thuyết, hãy để tôi lấy một ví dụ cụ thể: Biểu diễn phép tính 2 + 3 = 5 bằng functor.
Đầu tiên tôi sẽ xây dựng một class Wrapper nhận vào một giá trị, class này có hai methods: fmap
để biến đổi và indentity
để lấy ra giá trị:
class Wrapper {
constructor(value) {
this.value = value;
}
fmap(fn) {
return new Wrapper(fn(this.value));
}
identity() {
return this.value;
}
map(fn) {
return fn(this.value);
}
}
fmap
nhận vào một hàm, dùng hàm đó để biến đổi value
và lại đặt vào Wrapper
. identity
chỉ đơn giản là trả về value
.
Tôi sẽ sử dụng curry function để thực hiện phép cộng. Nếu chưa biết về curry bạn có thể đọc bài viết Curry function là gì? Một món “cà ri” ngon và làm sao để thưởng thức nó?.
const plus = a => b => a + b;
const plus3 = plus(3);
const two = new Wrapper(2);
const sum = two.fmap(plus3); // Wrapper(5)
sum.identity(); // 5
Đến đây thì các bạn có phát hiện ra điều gì thú vị không? Đúng rồi đó, sum vẫn có thể tiếp tục sử dụng được hàm fmap
hay nói cách khác là khi kết quả xử lý trả về một đối tượng là Wrapper thì chúng ta sẽ không phải lo lắng về tính liên tục của dữ liệu sau xử lý. Tôi có thể tiếp tục cộng trừ nhân chi một cách liên tiếp:
const multi = a => b => a * b;
const multi5 = multi(5);
sum.fmap(multi5).identity(); // 25
Khi kết quả của hàm fmap
trả về là một Wrapper
thì nó đảm bảo được rằng kết quả vẫn mang những tính chất của Wrapper.
Xem thêm các việc làm tuyển dụng Javascript hấp dẫn tại TopDev
Thật thú vị phải không? Ý tưởng về chuỗi các hàm có làm bạn liên tưởng đến hàm map
hay filter
trong Javascript? Thật vậy đó chính xác là những triển khai của functor.
map :: (A -> B) -> Array(A) -> Array(B)
filter :: (A -> Boolean) -> Array(A) -> Array(A)
map
và filter
được coi là functor bởi chúng có những đặc điểm của functor:
- Giống nhau
- Duy trì cấu trúc
- Loại giá trị
Functor cần phải đảm bảo được một số thuộc tính quan trọng:
Không gây ra side effect: có thể fmap
qua một hàm identity
để có được cùng một giá trị trong một ngữ cảnh. Điều này chứng minh được rằng chúng không gây ra side effect và vẫn bảo toàn cấu trúc của giá trị được bao bọc. Bạn có thể hiểu identity
là một hàm chỉ đơn giản là trả về giá trị mà nó nhận được.
Wrapper('Get Functional').fmap(x => x); // Wrapper('Get Functional')
Thứ hai, chúng phải có thể kết hợp được. Tức là có thể fmap
được liên tục. Để đảm bảo được điều này, các cấu trúc điều khiển ví dụ như fmap
phải không được ném ra exception, thay đổi các phần tử trong danh sách hoặc thay đổi hành vi của một hàm. Mục đích là tạo ra một ngữ cảnh cho phép bạn thao tác vào các giá trị mà không làm thay đổi giá trị ban đầu. Điều này thể hiện rõ ràng trong việc hàm map
biến đổi mảng này thành mảng khác mà không làm thay đổi mảng ban đầu.
Tuy nhiên trong lập trình không phải lúc nào ta cũng có dữ liệu hoàn hảo, mà chúng ta vẫn phải xử lý những exception, những giá trị như null, undefined… Lúc này việc áp dụng các functor sẽ không còn hoàn hảo nữa.
const div = a => b => b/a;
const subtr = a => b => a - b;
const plus = a => b => a + b;
const divided5 = div(5);
const subtr2 = subtr(2);
const plus3 = plus(3);
const two = Wrapper(2);
two.fmap(subtr2).fmap(divided5).fmap(plus3); // Wrapper(NaN)
Tổng kết
Functor là một cấu trúc dữ liệu lưu trữ dữ liệu ở trong một “vùng chứa”, nó cung cấp các phương thức để thao tác với dữ liệu ở trong “vùng chứa” đó. Sử dụng functor chúng ta sẽ đảm bảo được đầu ra của dữ liệu sẽ không bị thay đổi kiểu, nó giống với việc hàm map
nhận vào một array và luôn luôn trả ra một array.
Bài viết gốc được đăng tải tại 2coffee.dev
Có thể bạn quan tâm:
- Một số kinh nghiệm làm việc với mảng trong JavaScript dành cho bạn
- Tìm hiểu cấu trúc dữ liệu ArrayMap trong Android
- Thuật toán tham lam (Greedy Algorithm) – Thực hành với C++
Hàng loạt việc làm IT lương cao trên TopDev đang chờ bạn, ứng tuyển ngay!