Lớp kiểu MonadWriter
Trong Haskell, MonadWriter là một lớp kiểu (type class) cung cấp giao diện chung cho các Monad có khả năng ghi lại dữ liệu phụ (thường dùng cho logging hoặc audit trail). Lớp này yêu cầu kiểu dữ liệu ghi lại phải là một Monoid để có thể kết hợp các giá trị日志.
class (Monoid w, Monad m) => MonadWriter w m | m -> w where
writer :: (a, w) -> m a
writer ~(val, logData) = do
tell logData
return val
tell :: w -> m ()
tell logData = writer ((), logData)
listen :: m a -> m (a, w)
pass :: m (a, w -> w) -> m a
listens :: MonadWriter w m => (w -> b) -> m a -> m (a, b)
listens func action = do
~(val, logData) <- listen action
return (val, func logData)
censor :: MonadWriter w m => (w -> w) -> m a -> m a
censor func action = pass $ do
val <- action
return (val, func)
Các hàm chính trong lớp này bao gồm:
writer: Đóng gói một giá trị trả về và dữ liệu日志 vào Monad.tell: Ghi thêm dữ liệu vào日志 mà không trả về giá trị hữu ích (unit).listen: Thực thi một hành động và trả về cả kết quả lẫn dữ liệu日志 đã sinh ra.pass: Cho phép sửa đổi dữ liệu日志 dựa trên một hàm được trả về từ chính hành động.listensvàcensor: Là các hàm tiện ích để biến đổi日志 mà không cần truy cập trực tiếp vào cấu trúc bên trong.
Biến đổi Monad WriterT
WriterT là một Monad transformer, cho phép thêm khả năng ghi日志 vào một Monad nền khác. Nó được định nghĩa như một newtype bao bọc một cặp giá trị nằm trong Monad nội tại.
newtype WriterT w m a = WriterT { runWriterT :: m (a, w) }
instance (Monoid w, Monad m) => Monad (WriterT w m) where
return val = writer (val, mempty)
action >>= process = WriterT $ do
~(x, log1) <- runWriterT action
~(y, log2) <- runWriterT (process x)
return (y, log1 `mappend` log2)
writer :: (Monad m) => (a, w) -> WriterT w m a
writer = WriterT . return
Logic của instance Monad hoạt động như sau:
return: Tạo ra một giá trị với phần日志 là đơn vị trống (mempty).>>=(bind): Thực thi hành động đầu tiên để lấy giá trị và日志, sau đó áp dụng hàm tiếp theo. Cuối cùng, nó kết hợp hai phần日志 lại với nhau bằngmappend.
Chứng minh định luật Monad
Để đảm bảo tính đúng đắn, WriterT phải tuân thủ ba định luật Monad. Dưới đây là tóm tắt logic chứng minh:
- Left Identity:
return val >>= functương đươngfunc val. Vìreturntạo日志 rỗng, việc kết hợp nó với日志 củafunckhông làm thay đổi kết quả. - Right Identity:
action >>= returntương đươngaction. Hàmreturnkhông sinh thêm日志, nên tổng日志 giữ nguyên. - Associativity:
(action >>= f) >>= gtương đươngaction >>= (\x -> f x >>= g). Do tính chất kết hợp của phép nối Monoid ((w1 <> w2) <> w3 = w1 <> (w2 <> w3)), thứ tự nhóm các phép tính không ảnh hưởng đến kết quả cuối cùng.
Hàm nâng Lift
Để sử dụng WriterT như một Monad transformer chuẩn, nó cần implement lớp MonadTrans thông qua hàm lift.
instance (Monoid w) => MonadTrans (WriterT w) where
lift action = WriterT $ do
val <- action
return (val, mempty)
Hàm lift đưa một hành động từ Monad nền vào WriterT mà không sinh ra bất kỳ dữ liệu日志 nào.
Các hàm tiện ích trong WriterT
Ngoài các hàm trong lớp MonadWriter, module này còn cung cấp các hàm để thao tác trực tiếp với cấu trúc:
tell :: (Monad m) => w -> WriterT w m ()
tell logData = writer ((), logData)
listen :: (Monad m) => WriterT w m a -> WriterT w m (a, w)
listen action = WriterT $ do
~(val, logData) <- runWriterT action
return ((val, logData), logData)
pass :: (Monad m) => WriterT w m (a, w -> w) -> WriterT w m a
pass action = WriterT $ do
~((val, func), logData) <- runWriterT action
return (val, func logData)
execWriterT :: (Monad m) => WriterT w m a -> m w
execWriterT action = do
(_, logData) <- runWriterT action
return logData
Monad Writer
Writer thực chất là một trường hợp đặc biệt của WriterT khi Monad nền là Identity. Điều này giúp đơn giản hóa việc sử dụng khi không cần kết hợp với các hiệu ứng khác.
type Writer w = WriterT w Identity
runWriter :: Writer w a -> (a, w)
runWriter = runIdentity . runWriterT
execWriter :: Writer w a -> w
execWriter action = snd (runWriter action)
Ví dụ thực tế
Dưới đây là một ví dụ về việc sử dụng Writer để ghi lại quá trình tính toán. Thay vì chỉ nhân số, chúng ta sẽ mô phỏng một quy trình xử lý dữ liệu có ghi nhận từng bước.
import Control.Monad.Trans.Writer
auditValue :: Int -> Writer [String] Int
auditValue x = writer (x, ["Đã nhận giá trị: " ++ show x])
computeProduct :: Writer [String] Int
computeProduct = do
x <- auditValue 3
y <- auditValue 5
tell ["Đang tính toán tích của " ++ show x ++ " và " ++ show y ]
return (x * y)
main :: IO ()
main = print $ runWriter computeProduct
-- Kết quả: (15, ["Đã nhận giá trị: 3", "Đã nhận giá trị: 5", "Đang tính toán tích của 3 và 5"])
Trong ví dụ này, hàm tell được sử dụng để chèn thêm thông báo vào giữa quá trình thực thi, trong khi auditValue vừa trả về số liệu vừa ghi lại lịch sử nhận dữ liệu. Kết quả cuối cùng bao gồm cả giá trị tính toán và toàn bộ chuỗi sự kiện đã diễn ra.