[Functional Programming + Elm] Bài 4 - Functor & Applicative
Khi học bất kỳ ngôn ngữ lập trình nào, sau khi đã 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 mà chúng ta cần quan tâm 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 phổ biến, được triển khai nhờ khái niệm class
mà chúng ta đã nói tới trước đó; Và ở đây chúng ta sẽ tìm hiểu pattern
đầu tiên 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 HOF
phổ biến nhất trong môi trường 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 một hàm xử lý dữ liệu ví dụ như Char.toCode
và truyền vào một giá trị c : Char
thì chúng ta lại thường có logic hoạt động của code ở runtime
sẽ là Char.map f c
, với f
được truyền tới từ đâu đó và có thể sẽ là Char.toCode
.
Bằng cách sử dụng map
làm HOF
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áctype
có thể được đối chiếu bởimap
.
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. Và bây giờ chúng ta sẽ vay mượn định nghĩa class Functor
sang Elm
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 (..)
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
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 đó, chúng ta cứ tiến hành viết code triển khai hàm Book.map
trước, để suy nghĩ về thao tác khi sử dụng hàm map
và chọn định kiểu phù hợp cho các yếu tố.
Ở đâ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
; Sau đó thì việc áp dụng func
cho các trường dữ liệu nào sẽ do logic của map
quy định. Hoặc, cũng 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 khi đã có thông tin định kiểu của hàm map
là (Book -> any) -> Book -> any
, thì chúng ta đặt 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.
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
. 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
-- Expected: Just 10 : Maybe number
-- Error: cannot find `Maybe.apply`
Chúng ta có thể đọc thao tác Maybe.apply
ở đây là - áp dụng logic của Hàm đang được lưu trữ trong maybeFunc
lên giá trị đang được 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 các lambda
và có khả năng tạo tương tác với chính kiểu wrapper
đó bằng hàm HOF
là 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 các 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 Extension.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 (..)
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 Extension.Maybe
và viết code chi tiết cho apply
và map
để có kết quả hoạt động như dự kiến.
module Extension.Maybe exposing (..)
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 maybe =
case maybe 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. 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.
// -- zip : [a] -> [b] -> [[a, b]]
Array.prototype.zip = function (thatArray) {
var thisArray = this;
var pairCounter = (thisArray.length < thatArray.length)
? thisArray.length
: thatArray.length;
// -- pairing
var accumulator = new Array (pairCounter);
for (var i = 0; i < pairCounter; i += 1) {
var thisValue = thisArray[i];
var thatValue = thatArray[i];
accumulator[i] = [thisValue, thatValue];
}
return accumulator;
}
// -- zip-test
var fst = ['a','b','c','d','e','f','g','h','i'];
var snd = [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21];
var zip = fst.zip (snd);
console.log ("-- zip-test :");
console.log (zip);
// -- zip-test :
// [['a',1],['b',2],['c',3],['d',4],['e',5],['f',6],['g',7],['h',8],['i',9]]
// -- apply : [(a -> b)] -> [a] -> [b]
Array.prototype.apply = function (dataArray) {
var funcArray = this;
var pairArray = funcArray.zip (dataArray);
// -- applying
return pairArray.map ((pair) => {
var [func, data] = pair;
return func (data);
});
}
// -- apply-test
var add = (a) => (b) => a + b;
var funcArray = [add(1),add(2),add(3),add(4),add(5),add(6),add(7),add(8),add(9)];
var dataArray = [ 8 , 7 , 6 , 5 , 4 , 3 , 2 , 1 , 0 ];
var resultArray = funcArray.apply (dataArray);
console.log ("-- apply-test :");
console.log (resultArray);
// -- apply-test :
// [9,9,9,9,9,9,9,9,9]
All Rights Reserved