Functor là gì? Tôi có cần biết đến functor?

Functor là gì? Tôi có cần biết đến functor?

Những mẩu tin ngắn hàng ngày dành cho bạn
  • Mới từ quê lên, chưa kịp cập nhật biến động "thị trường" nên thôi... Mai mình "lên bài" bù nhé 😅

    » Xem thêm
  • Một phần mềm giúp chuyển đổi văn bản thành giọng nói do một lập trình viên người Việt làm ra - J2TEAM - Text to Speech (Free). Bạn có thể chuyển đổi hàng chục ngôn ngữ sang hàng chục giọng đọc tự nhiên khác nhau. Điều đặc biệt là nó miễn phí.

    Đánh giá sơ bộ thì chuyển đổi văn bản dài hoặc văn bản bằng tiếng Việt thuần thì rất tốt. Còn dính thêm các từ tiếng Anh thì nó đọc hơi buồn cười 😅

    » Xem thêm
  • Quá ghê ghớm, Codeium - vốn được biết đến như một đối thủ của Github Copilot, khi nó cho người dùng dùng miễn phí không giới hạn. Mới đây họ giới thiệu thêm Windsurf Editor - không chỉ còn là VSCode Extentions nữa mà là một trình Editor luôn - cạnh tranh trực tiếp với Cursor. Và điểm chính là nó... hoàn toàn miễn phí 🫣.

    » Xem thêm

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:

Wrapper

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.

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)

mapfilter đượ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.

Cao cấp
Hello

Tôi & khao khát "chơi chữ"

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Bạn đã thử viết? Và rồi thất bại hoặc chưa ưng ý? Tại 2coffee.dev chúng tôi đã có quãng thời gian chật vật với công việc viết. Đừng nản chí, vì giờ đây chúng tôi đã có cách giúp bạn. Hãy bấm vào để trở thành hội viên ngay!

Xem tất cả

Đăng ký nhận thông báo bài viết mới

hoặc
* Bản tin tổng hợp được gửi mỗi 1-2 tuần, huỷ bất cứ lúc nào.
Author

Xin chào, tôi tên là Hoài - một anh Dev kể chuyện bằng cách viết ✍️ và làm sản phẩm 🚀. Với nhiều năm kinh nghiệm lập trình, tôi đã đóng góp một phần công sức cho nhiều sản phẩm mang lại giá trị cho người dùng tại nơi đang làm việc, cũng như cho chính bản thân. Sở thích của tôi là đọc, viết, nghiên cứu... Tôi tạo ra trang Blog này với sứ mệnh mang đến những bài viết chất lượng cho độc giả của 2coffee.dev.Hãy theo dõi tôi qua các kênh LinkedIn, Facebook, Instagram, Telegram.

Bạn thấy bài viết này có ích?
Không

Bình luận (2)

Nội dung bình luận...
Avatar
Update Group1 năm trước
Làm về monad, applicative, monoid chưa ạ?
Trả lời
Avatar
Xuân Hoài Tống1 năm trước
Chào bạn, cảm ơn bạn đã theo dõi mình suốt thời gian qua. Hiện tại mình vẫn đang bận rộn với những kế hoạch đề ra từ trước nên trước mắt vẫn chưa hoàn thành bài viết này được. Mong bạn thông cảm và tiếp tục ủng hộ :D
Avatar
Update Group2 năm trước
Làm về monad, applicative, monoid đi ạ
Trả lời
Avatar
Update Group1 năm trước
Làm về monad, applicative, monoid có chưa ạ
Avatar
Xuân Hoài Tống2 năm trước
Hi bạn, mình cũng đang trong quá trình tìm hiểu về lập trình hàm thôi nên nếu cảm thấy đủ hiểu thì mình sẽ viết các bài chi tiết