Khám phá MonadWriter và WriterT trong Haskell

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.
  • listenscensor: 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ằng mappend.

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:

  1. Left Identity: return val >>= func tương đương func val. Vì return tạo日志 rỗng, việc kết hợp nó với日志 của func không làm thay đổi kết quả.
  2. Right Identity: action >>= return tương đương action. Hàm return không sinh thêm日志, nên tổng日志 giữ nguyên.
  3. Associativity: (action >>= f) >>= g tương đương action >>= (\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.

Thẻ: Haskell monad-transformer writer-monad functional-programming

Đăng vào ngày 11 tháng 6 lúc 23:25