Functor & Applicative

Tram Ho

Đối với bất kỳ ngôn ngữ lập trình nào khi chúng ta đã nắm bắt được các kiểu dữ liệu cơ bản và cú pháp hỗ trợ khai báo trừu tượng Abstraction để tạo ràng buộc khi thiết kế tổng quan phần mềm, thì bước tiếp theo là tìm hiểu về một số dạng thức triển khai pattern để áp dụng cho tiến trình thiết kế. Trong môi trường Functional nói riêng thì chúng ta có một số pattern rất đơn giản và phổ biến được triển khai nhờ các class mà chúng ta đã nói tới trước đó. Và pattern đầu tiên mà chúng ta sẽ tìm hiểu là có tên là Functor.

Functor

Xuất phát với hàm map mà chúng ta đã biết trong module List, thực tế thì đây là hàm phổ biến nhất trong Functional bởi có rất nhiều trường hợp chúng ta sẽ không viết những lời gọi hàm trực tiếp làm việc với kiểu dữ liệu nào đó. Tức là thay vì trực tiếp gọi hàm Char.toCode và truyền vào một giá trị c : Char thì chúng ta lại viết là Char.map f c với f được truyền tới từ đâu đó và có thể f sẽ là Char.toCode.

Bằng cách sử dụng map làm HOD và điều khiển việc gọi hàm trên c : Char như vậy thì chúng ta sẽ có thể kiến trúc chương trình linh động hơn. Và kiểu Char nếu được định nghĩa hàm map để có thể viết code triển khai như vậy thì sẽ được gọi là một kiểu thuộc class Functor.

Functorclass bao gồm các kiểu dữ liệu có thể được đối chiếu bởi hàm map.

Trong Haskell hay PureScript thì Type Class được hỗ trợ ở cấp độ cú pháp và có rất nhiều Type Class đã được định nghĩa sẵn trong thư viện tiêu chuẩn, bao gồm cả Functor mà chúng ta đang nói tới ở đây. Bây giờ chúng ta sẽ định nghĩa class Functor cho bất kỳ kiểu dữ liệu a nào tham gia vào sẽ có thể map để đối chiếu các giá trị tới kiểu b bất kỳ.

Và code triển khai để sử dụng vẫn class Functor sẽ được viết trong module Book ở ví dụ trước đó.

Về việc gắn các thông tin định kiểu ở dòng instanceFunctor : Class.Functor Book any Book any cho các vị trí Type Variable thì ban đầu chúng ta có thể viết instanceFunctor : Class.Functor a b c d. Sau đó cứ tiến hành viết code triển khai code hàm map trước để suy nghĩ về thao tác khi sử dụng hàm map.

Ở đây chúng ta có thể thiết kế để map nhận vào hàm func không có hiểu biết gì về kiểu Book và chỉ làm việc trên các kiểu primitive và sau đó việc áp dụng func cho trường dữ liệu nào sẽ do logic của map quy định; Hoặc có thể thiết kế để map nhận vào hàm func chứa logic làm việc trực tiếp với kiểu Book như trên.

Sau đó chúng ta đặt các thông tin định kiểu tương ứng của hàm map : (a -> b) -> c -> d ngược lại về định nghĩa của instanceFunctor để đảm bảo logic triển khai class Functor được nhất quán và trình biên dịch sẽ không báo lỗi.

Và như vậy là chúng ta đã có thể sử dụng Book.map ở bất kỳ vị trí nào trong chương trình và truyền vào một lambda đối chiếu mà không cần viết code định nghĩa thêm các hàm đối chiếu trong module Book.

Đó là Functor. Rất đơn giản và linh động. Bây giờ chúng ta hãy nói về Applicative.

Applicative

Khái niệm Applicative có tên gọi đầy đủ là Applicative Functor, và được sử dụng để nói về các kiểu Functor là các kiểu cấu trúc dữ liệu có thể lưu trữ một hoặc nhiều hàm f khác nhau.

Ví dụ như một Maybe có thể có chứa một hàm (number -> number) để tương tác với các giá trị số học.

À… và điều kiện kèm theo là maybeFunc như mô tả ở trên cần phải có thể được áp dụng lên một maybeNumb để trả về một giá trị cùng kiểu Maybe number như sau:

Chúng ta có thể đọc thao tác Maybe.apply ở đây là – áp dụng logic hàm lưu trữ trong maybeFunc lên giá trị lưu trữ trong maybeNumb. Và trong trường hợp này thì kiểu dữ liệu Maybe được xem là một Applicative Functor hay là một thành viên của class Applicative.

Như vậy là chúng ta có Applicative là các kiểu dữ liệu dạng vỏ bọc wrapper như Maybe, List, v.v… có thể lưu trữ các hàm có tên hoặc lambda và tạo tương tác với chính kiểu wrapper đó bằng hàm apply. Logic xử lý của apply ở đây là tách lấy các hàm f đang được lưu trữ trong wrapper đầu tiên và map sang giá trị đang được lưu trữ trong wrapper thứ hai.

Phần code ví dụ ở trên chỉ là để mô phỏng cú pháp sử dụng và định nghĩa Applicative, còn trên thực tế thì chúng ta không có hàm apply trong module Maybe để sử dụng như vậy. Việc viết thêm các hàm mở rộng cho module Maybe là không khả thi trong môi trường Elm và chúng ta sẽ phải tạo ra một module MaybeExt để mở rộng thêm tính năng cho kiểu Maybe sẵn có.

Tuy nhiên trước hết hãy bắt đầu với việc định nghĩa class Applicative trong Elm bằng record như chúng ta đã định nghĩa class Functor trước đó.

Ở đây chúng ta vẫn có yếu tố kế thừa từ Functor là hàm map, tuy nhiên trong Applicative thì apply quan trọng hơn và sẽ được sử dụng làm mốc triển khai logic trước. Bây giờ chúng ta thực hiện khai báo instance trong module MaybeExt và viết code chi tiết cho applymap để có kết quả hoạt động như dự kiến.

Điểm đầu tiên cần lưu ý trong code triển khai là chúng ta có hàm maybeFunc chỉ đảm nhiệm vai trò làm việc với giá trị được đặt bên trong kiểu wrapper và không có hiểu biết gì về cấu trúc của wrapper được sử dụng là Maybe, List, hay Record, v.v…

Như vậy hàm apply : a -> b -> c khai báo trong class Applicative sẽ có thông tin định kiểu cụ thể là tham số đầu tiên có dạng hàm (a -> b) được đặt trong wrapper Maybe. Logic xử lý của apply sẽ nhận vào một Maybe tiếp theo có chứa dữ liệu là giá trị thuộc kiểu a nào đó tương thích với maybeFunc. Và kết quả trả về là một Maybe mới chứa giá trị thuộc kiểu b cũng tham chiếu từ maybeFunc.

Lúc này chúng ta đã có thể viết các thông tin định kiểu cụ thể này vào vị trí khai báo instance và vẫn để các Type Variable còn lại là de chưa biết chính xác. Sau đó tiếp tục viết code cho hàm map:

Xuất phát từ vị trí Functor là tham số thứ hai Maybe a để đối chiếu tới giá trị mới là Maybe b. Như vậy hàm func sẽ có thông tin định kiểu là (a -> b) để có logic phù hợp. Và chúng ta đang có khai báo trong class Applicativemap : (d -> e) -> b -> c. Như vậy tổng kết lại chúng ta sẽ có thông tin định kiểu đầy đủ cho thao tác khai báo instance... là:

Bây giờ chúng ta đã có thể sử dụng elm reactor hoặc elm repl để kiểm tra hoạt động của apply.

Một ví dụ khác về Applicative là tạo apply cho kiểu List. Giả sử chúng ta có một funcList chứa các hàm func : (number -> number) và một numbList khác chứa các giá trị number như sau:

Với dạng thức triển khai Applicative như trên thì chúng ta sẽ có thể tạo apply để áp dụng các hàm ở funcList lên các giá trị ở numbList với tỉ lệ 1:1 và trả về mảng kết quả như sau:

Logic của map ở đây vẫn sẽ duy trì giống như List.map sẵn có và không cần phải định nghĩa lại. Tuy nhiên apply thì sẽ có khá nhiều thao tác cần thực hiện thêm so với trường hợp của Maybe để có logic hoạt động như vậy. Bạn có thể sử dụng trường hợp này để luyện tập triển khai Applicative và chúng ta sẽ tạm dừng tại đây để chuyển sang những khái niệm Functional tiếp theo.

Chia sẻ bài viết ngay

Nguồn bài viết : Viblo