- vừa được xem lúc

[Functional Programming + Elm] Bài 5 - Monad & Monoid

0 0 11

Người đăng: Semi Dev

Theo Viblo Asia

Tổng kết từ bài viết trước đó thì chúng ta mới có thêm các công cụ là: class Functor để triển khai hàm điều khiển map cho một kiểu dữ liệu bất kỳ bao gồm cả các kiểu primitive và các kiểu có cấu trúc, và class Applicative để áp dụng các cấu trúc dữ liệu tổ hợp đang lưu trữ cục bộ một hàm f lên một giá trị khác cùng kiểu cấu trúc giống như đang trực tiếp sử dụng hàm f đó.

Trong bài viết này chúng ta sẽ tìm hiểu về các pattern có tên là MonadMonid cũng rất phổ biến trong môi trường Functional. Chúng ta hãy đầu với Monad, tiếp tục là một class mở rộng từ dòng Functor -> Applicative.

Monad

Ở đây chúng ta sẽ nhắc lại một chút về các class đã biết trước đó. Đầu tiên chúng ta có class Functor được sử dụng để tạo ràng buộc triển khai hàm map để làm trình điều khiển HOF cho một kiểu dữ liệu bất kỳ.

Sau đó thì Applicative mở rộng từ Functor với ràng buộc bổ sung là yêu cầu viết code triển khai cho hàm apply, và điểm đặc biệt của apply là cho phép chúng ta áp dụng tính năng của một hàm đang được lưu trữ bên trong một cấu trúc dữ liệu vỏ bọc lên một cấu trúc dữ liệu khác cùng kiểu.

Và bây giờ chúng ta có thêm Monad tiếp tục mở rộng từ Applicative với ràng buộc bổ sung là yêu cầu viết code triển khai cho hàm bind, để có thể truyền một giá trị Wrapped a vào một hàm func : (a -> Wrapped b). Điểm lưu ý ở đây là chúng ta đang có một hàm bất kỳ func được thiết kế để nhận vào một giá trị a và trả về giá trị b được đặt trong vỏ bọc Wrapped so với kiểu của a.

Điều đó có nghĩa là thao tác bind sẽ phải phân tích cấu trúc của Wrapped a để tách lấy a và truyền vào hàm func. Như vậy chúng ta có định nghĩa class Monad như sau.

module Class exposing (YesNo, Functor, Applicative, Monad) type alias Monad a b c d e x y = { bind : a -> (b -> c) -> d , apply : e -> a -> c , map : (x -> y) -> a -> c } type alias Applicative a b c d e = { apply : a -> b -> c , map : (d -> e) -> b -> c } type alias Functor a b c d = { map : (a -> b) -> c -> d } -- type alias YesNo ...

Do không được hỗ trợ ở cấp độ cú pháp của ngôn ngữ nên chúng ta vẫn phải viết định nghĩa kèm theo các hàm applymap được kế thừa từ các class trước đó; Và mốc xuất phát để đặt tên các Type Variable sẽ ưu tiên từ hàm bind mới xuất hiện.

Sau đó ở phần code triển khai chúng ta sử dụng lại code của module MaybeExt của bài viết trước. Chúng ta đã có các hàm applymap hoạt động tốt khi triển khai class Applicative. Bây giờ chúng ta sẽ thay bước khai báo class từ Applicative thành Monad.

module MaybeExt exposing (map, apply, bind)
import Class exposing (..) instanceMonad : Monad a b c d e x y
instanceMonad = Monad bind apply map -- bind : Not Implemented ...
-- apply : Done ...
-- map : Done ...

Hàm bind mà chúng ta cần triển khai đầu tiên sẽ nhận vào một giá trị a trong kiểu vỏ bọc Wrapped, và như vậy chúng ta có thông tin định kiểu là Maybe a. Tham số tiếp theo là hàm func : (a -> Wrapped b) sẽ có thông tin định kiểu tương ứng là (a -> Maybe b).

bind : (Maybe a) -> (a -> Maybe b) -> (Maybe b)

Logic xử lý trong hàm là chúng ta có trường hợp Maybe a có thể là Nothing hoặc Just a. Trong trường hợp hàm func nhận bindnhận được Nothing thì sẽ không áp dụng hàm func mà trả về luôn Nothing, và trường hợp còn lại thì chúng ta tách lấy giá trị a trong pattern và truyền vào func.

bind : (Maybe a) -> (a -> Maybe b) -> (Maybe b)
bind maybeAny func = case maybeAny of Nothing -> Nothing Just any -> func any

Và từ thông tin định kiểu của các hàm chúng ta có thể đối chiếu ngược lại để đặt thông tin định kiểu cụ thể cho các Type Variable trong phần khai báo instanceMonad.

instanceMonad : Monad (Maybe a) a (Maybe b) (Maybe b) (Maybe (a -> b)) a b
instanceMonad = Monad bind apply map

Và tổng kết code của module MaybeExt:

module MaybeExt exposing (map, apply, bind)
import Class exposing (..) instanceMonad : Monad (Maybe a) a (Maybe b) (Maybe b) (Maybe (a -> b)) a b
instanceMonad = Monad bind apply map bind : (Maybe a) -> (a -> Maybe b) -> (Maybe b)
bind maybeAny func = case maybeAny of Nothing -> Nothing Just any -> func any apply : Maybe (a -> b) -> Maybe a -> Maybe b
apply maybeFunc maybeAny = case maybeFunc of Nothing -> Nothing Just func -> map func maybeAny map : (a -> b) -> Maybe a -> Maybe b
map func maybeAny = case maybeAny of Nothing -> Nothing Just any -> Just (func any)

Bây giờ chúng ta lại mở Elm REPL để thử sử dụng MaybeExt.bind.

cd learn-elm
elm repl ---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> import MaybeExt exposing (..) > maybeAny = Just 9 Just 9 : Maybe number > MaybeExt.bind maybeAny (\n -> Just (n * 9))
Just 81 : Maybe number

Ý nghĩa căn bản của việc sử dụng Monad như bạn đã thấy đó là có thêm hàm bind để cho phép chúng ta gắn một cấu trúc dữ liệu có thể ở dạng phức tạp vào một hàm vô danh lamda với định nghĩa rất ngắn gọn; Bởi logic phân tích cấu trúc dữ liệu đã được tổng quát tại hàm bind và như vậy lamda sẽ chỉ thể hiện logic xử lý chính mà chúng ta mong muốn.

Đây là điểm mà chúng ta có thể suy nghĩ tới trong việc áp dụng sang một môi trường khác như JavaScript, các logic rẽ nhánh dựa trên trạng thái của các cấu trúc dữ liệu thực ra lặp lại rất nhiều và có thể được tổng quát vào các HOF như đã thấy.

Trong số các kiểu dữ liệu có cấu trúc của JavaScript thì mới chỉ có duy nhất Array được định nghĩa nhiều HOF có bao gồm cả map, tuy nhiên vẫn chưa có apply như code JavaScript ví dụ ở cuối bài viết trước và bind với logic tổng quát như Monad ở đây.

Bạn có thể thử code bổ sung các HOF này cho class Setclass Map trong JavaScript và sử dụng. Chắc chắn là các HOF sẽ rất hữu ích và giảm thiểu được rất nhiều các thao tác chuyển đổi dữ liệu về các mảng Array để sử dụng các HOF sẵn có, đặc biệt là đối với trường hợp của class Set.

Để tránh nhầm lẫn thì applybind của class Function trong JavaScript không có ý nghĩa tương đương với các hàm cùng tên của ApplicativeMonad ở đây; Bởi đó chỉ là các phương thức thay thế cho cú pháp gọi hàm thông thường hoặc gắn kèm địa chỉ tham chiếu của object vào hàm có nội dung cần sử dụng con trỏ this.

Các HOF mà chúng ta đang nói tới thuộc về các module của các cấu trúc dữ liệu, và sẽ có logic triển khai chi tiết khác nhau tùy vào cấu trúc của mỗi kiểu dữ liệu. Nếu như chúng ta triển khai các HOF trong JavaScript thì sẽ ở dạng các phương thức của các object dữ liệu giống như thisArray.apply(thisToThat) hoặc các phương thức static của các class để có cú pháp sử dụng giống như Elm ở đây.

Monoid

Khái niệmMonoid khá đơn giản, nhưng bản thân mình không còn vốn từ vựng của môn Toán nên cũng không biết phải dịch tên mô tả thế nào. Tuy nhiên, để mô tả bản chất thì chúng ta có thể hiểu một giao diện Monoid sẽ bao gồm các thành phần có tên là một hàm liên kết Associative, một giá trị đặc trưng Identity, và một hàm điều khiển thực hiện liên kết một danh sách các giá trị sử dụng Associative.

Đầu tiên chúng ta có Associative là một phép thực thi thỏa mãn điều kiện là các giá trị xoay quanh có được sắp xếp thế nào cũng sẽ không ảnh hưởng tới kết quả cuối cùng; Ví dụ như phép cộng +, phép nhân *, v.v... hoặc một phép thực thi mô tả tương tác trong phần mềm không nhất thiết phải liên quan tới Toán học Ví dụ:

(a + b) + c
a + (b + c)
(a + c) + b hoặc (a * b) * c
a * (b * c)
(a * c) * b

Yếu tố tiếp theo, Identity là một giá trị vô nghĩa đối với phép thực thi Associative; Ví dụ chúng ta có giá trị 0 vô nghĩa với phép tính + bởi 0 + x = x, hoặc giá trị 1 vô nghĩa với phép tính * bởi 1 * x = x. Và yếu tố còn lại là hàm điều khiển thì không thuộc về định nghĩa Monoid trong Toán Học nên chúng ta sẽ nói đến ở phần code ví dụ sau đây.

Trước hết chúng ta sẽ có định nghĩa tổng quát của giao diện Monoid với tên đại diện của các yếu tố được đặt theo quy ước chung trong môi trường Functional Programming và lần lượt là append, empty, và concat.

module Class exposing (..) type alias Monoid a = { append : a -> a -> a , empty : a , concat : List a -> a } -- type alias Monad ...
-- type alias Applicative ...
-- type alias Functor ...

Với các ví dụ 0 + x = x1 * x = x thì chúng ta có thể thấy rằng kiểu dữ liệu number hay bất kỳ kiểu dữ liệu nào khác cũng đều có thể có nhiều giao diện Monoid; Cứ miễn sao chúng ta chỉ ra được một cặp Associative & Identity là chắc chắn sẽ có thể viết được hàm điều khiển liên kết danh sách các giá trị để sử dụng.

Như vậy chúng ta có thể tạo ra các module riêng dành cho mỗi Monoid để có cú pháp sử dụng trực quan, thay vì tập trung tất cả các Monoid vào một module ví dụ như SumProduct thay vì NumberExt. Bây giờ chúng ta sẽ xem xét code ví dụ của module Sum với phép liên kết Associative là phép tính cộng + và giá trị đặc trưng vô nghĩa Identity0.

module Sum exposing (..)
import Class exposing (..) instanceMonoid : Class.Monoid number
instanceMonoid = Monoid append empty concat append : number -> number -> number
append a b = a + b empty : number
empty = 0 concat : List number -> number
concat numberList = case numberList of first :: restList -> append first (concat restList) [] -> empty

Ở vị trí của hàm điều khiển concat chúng ta đang sử dụng AssociativeIdentity để tính tổng của một danh sách các giá trị số học. Đây chính là ứng dụng của Monoid. Do môi trường thuần Declarative không có cú pháp vòng lặp nên giải thuật đệ quy là phương thức duy nhất để thực hiện công việc tính tổng của một danh sách, và tất cả những trường hợp khác khi chúng ta cần chuyển đổi một danh sách thành một giá trị biểu trưng duy nhất.

Để áp dụng giải thuật đệ quy thì chúng ta cần xác định những yếu tố mà chính là các thành phần AssociativeIdentity. Ví dụ như khi tính tổng thì chúng ta xem phép tính (+)Associative và cần chỉ ra Identity tương ứng là 0 để làm điểm dừng edge case cho logic rẽ nhánh mỗi lần lặp; Hoặc khi tính tích thì chúng ta xem phép tính (*)Associative và cần chỉ ra Identity1 tại edge case.

> import Sum exposing (..) > Sum.append 9 0
9 : number > Sum.empty
0 : number > Sum.concat [1,2,3,4,5,6,7,8,9] 45 : number

Hiển nhiên là trong trường hợp xử lý dữ liệu thực tế thì chúng ta có thể sẽ muốn sử dụng các HOF có sẵn để thay cho việc định nghĩa hàm đệ quy dài dòng; Ví dụ như List.foldr hoặc List.foldl, hoặc trong JavaScript thì là array.reduce, _.reduceRight. Và hàm concant lúc này sẽ có thể được viết ở dạng tổng quát chung có thể copy/paste cho các module triển khai Monoid.

concat dataList = List.foldr append empty dataList

Ok, như vậy là chúng ta đã điểm qua hết những khái niệm căn bản của Functional Programming. Bây giờ thì chúng ta đã có thể tiếp tục tìm hiểu cách sử dụng Elm để xây dựng ứng dụng trang đơn SPA - Single Page Application. Ở bài viết cuối cùng của Sub-Series Declarative thì chúng ta đã phải tạm dừng ở đoạn tìm hiểu về các công cụ hỗ trợ phân tích cấu trúc URL để chuyển qua về các HOD ở Sub-Series này.

Để thuận tiện cho việc theo dõi và đọc lại các kiến thức liên quan về kiến trúc Elm Architecture và trình điều khiển Browser.Element thì mình sẽ viết bài mới trong nối tiếp ở phần đang tạm dừng của Sub-Series cũ. Sau khi xây dựng xong một SPA đơn giản thì chúng ta sẽ xem xét việc có nên quay trở lại Sub-Series Functional này và khởi tạo thêm một project Elm Fullstack hay không. 😄

(chưa đăng tải) [Declarative Programming + Elm] Bài 16 - URL Parser Module

Bình luận

Bài viết tương tự

- vừa được xem lúc

Closure trong Javascript - Phần 2: Định nghĩa và cách dùng

Các bạn có thể đọc qua phần 1 ở đây. Để mọi người không quên, mình xin tóm tắt gọn lại khái niệm lexical environment:.

0 0 51

- vừa được xem lúc

Var vs let vs const? Các cách khai báo biến và hằng trong Javascript

Dạo này mình tập tành học Javascript, thấy có 2 cách khai báo biến khác nhau nên đã tìm tòi sự khác biệt. Nay xin đăng lên đây để mọi người đọc xong hy vọng phân biệt được giữa let và var, và sau đó là khai báo hằng bằng const.

0 0 31

- vừa được xem lúc

VueJS: Tính năng Mixins

Chào mọi người, hôm nay mình sẽ viết về Mixins và 1 số vấn đề trong sử dụng Mixins hay ho mà mình gặp trong dự án thực. Trích dẫn từ trang chủ của VueJS:.

0 0 27

- vừa được xem lúc

Asset Pipeline là cái chi chi?

Asset Pipeline. Asset pipeline là cái chi chi. . Giải thích:.

0 0 47

- vừa được xem lúc

Tạo data table web app lấy dữ liệu từ Google Sheets sử dụng Apps Script

Google Sheets là công cụ tuyệt vời để lưu trữ bảng tính trực tuyến, bạn có thể truy cập bảng tính bất kỳ lúc nào ở bất kỳ đâu và luôn sẵn sàng để chia sẻ với người khác. Bài này gồm 2 phần.

0 0 266

- vừa được xem lúc

Học Deep Learning trên Coursera miễn phí

Bạn muốn bắt đầu với Deep Learning nhưng không biết bắt đầu từ đâu? Bạn muốn có một công việc ở mức fresher về Deep Learning? Bạn muốn khoe bạn bè về kiến thức Deep Learning của mình. Bắt đầu từ đâu.

0 0 35