Đố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
.
Functor
làclass
bao gồm các kiểu dữ liệu có thể được đối chiếu bởi hàmmap
.
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ỳ.
module Class exposing (YesNo, Functor) type alias Functor a b c d = { map : (a -> b) -> c -> d } -- type alias YesNo ...
Và code triển khai để sử dụng vẫn class Functor
sẽ được viết trong module Book
ở ví dụ trước đó.
module Book exposing (Book, yesno, map)
import Class exposing (..) type alias Book = { title : String , author : String , rating : Float } instanceFunctor : Class.Functor Book any Book any
instanceFunctor = Class.Functor map map : (Book -> any) -> Book -> any
map func abook = func abook -- instanceYesNo ...
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
.
module Main exposing (main)
import Book exposing (..)
import Html exposing (Html, text) main : Html message
main = let yogaBook = (Book "Yoga" "Patanjali" 9.9) showRating = (\abook -> "Rating: " ++ String.fromFloat abook.rating) in text <| Book.map showRating yogaBook -- "Rating: 9.9"
Đó 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.
> increment = (+)1
<function> : number -> number > maybeFunc = Just increment
Just <function> : Maybe (number -> number)
À... 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:
> maybeNumb = Just 9
Just 9 : Maybe number > Maybe.apply maybeFunc maybeNumb
-- Error: cannot find `Maybe.apply`
-- Expected: Just 10 : Maybe number
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 đó.
module Class exposing (YesNo, Functor, Applicative) type alias Applicative a b c d e = { apply : a -> b -> c , map : (d -> e) -> b -> c } -- type alias Functor ...
-- type alias YesNo ...
Ở đâ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 apply
và map
để có kết quả hoạt động như dự kiến.
module MaybeExt exposing (map, apply)
import Class exposing (..) instanceApplicative : Applicative a b c d e
instanceApplicative = Applicative apply map
Đ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...
-- instance ... apply : Maybe (a -> b) -> Maybe a -> Maybe b
apply maybeFunc maybeAny = case maybeFunc of Nothing -> maybeAny Just func -> map func maybeAny
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à d
và e
chưa biết chính xác. Sau đó tiếp tục viết code cho hàm map
:
-- apply : ... map : (a -> b) -> Maybe a -> Maybe b
map func maybeAny = case maybeAny of Nothing -> Nothing Just any -> Just (func any)
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 Applicative
là map : (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à:
instanceApplicative : Applicative (Maybe (a -> b)) (Maybe a) (Maybe b) a b
instanceApplicative = Applicative apply map
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
.
> maybeFunc = Just ((+) 1)
Just <function> : Maybe (number -> number) > maybeAny = Just 9
Just 9 : Maybe number > MaybeExt.apply maybeFunc maybeAny
Just 10 : Maybe number
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:
> add : number -> number -> number
> add a b = a + b > funcList = [add 1,add 2,add 3,add 4,add 5,add 6,add 7,add 8,add 9]
> [ ... ] : List (number -> number) > numbList = [ 8, 7, 6, 5, 4, 3, 2, 1, 0]
> [ ... ] : List number
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:
> ListExt.apply funcList numbList
[9,9,9,9,9,9,9,9,9] : List number
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.
(chưa đăng tải) [Functional Programming + Elm] Bài 5 - Monoid & Monad