Đừng sợ! Đơn nguyên trình đọc thực sự không quá phức tạp và có tiện ích thực sự dễ sử dụng.
Có hai cách để tiếp cận một đơn nguyên: chúng ta có thể hỏi
- Đơn nguyên làm gì ? Nó được trang bị những thao tác gì? Nó tốt cho cái gì?
- Đơn nguyên được thực hiện như thế nào? Nó phát sinh từ đâu?
Từ cách tiếp cận đầu tiên, đơn nguyên của người đọc là một số loại trừu tượng
data Reader env a
như vậy mà
-- Reader is a monad
instance Monad (Reader env)
-- and we have a function to get its environment
ask :: Reader env env
-- finally, we can run a Reader
runReader :: Reader env a -> env -> a
Vậy chúng ta sử dụng cái này như thế nào? Chà, đơn nguyên của trình đọc rất tốt để chuyển thông tin cấu hình (ngầm định) thông qua một phép tính.
Bất cứ khi nào bạn có một "hằng số" trong một phép tính mà bạn cần ở nhiều điểm khác nhau, nhưng thực sự bạn muốn có thể thực hiện cùng một phép tính với các giá trị khác nhau, thì bạn nên sử dụng đơn nguyên đọc.
Các monads của trình đọc cũng được sử dụng để thực hiện những gì người OO gọi là tiêm phụ thuộc . Ví dụ: thuật toán negamax được sử dụng thường xuyên (trong các hình thức được tối ưu hóa cao) để tính toán giá trị của một vị trí trong trò chơi hai người chơi. Mặc dù vậy, bản thân thuật toán không quan tâm đến trò chơi mà bạn đang chơi, ngoại trừ việc bạn cần phải xác định được vị trí "tiếp theo" trong trò chơi và bạn cần biết vị trí hiện tại có phải là vị trí chiến thắng hay không.
import Control.Monad.Reader
data GameState = NotOver | FirstPlayerWin | SecondPlayerWin | Tie
data Game position
= Game {
getNext :: position -> [position],
getState :: position -> GameState
}
getNext' :: position -> Reader (Game position) [position]
getNext' position
= do game <- ask
return $ getNext game position
getState' :: position -> Reader (Game position) GameState
getState' position
= do game <- ask
return $ getState game position
negamax :: Double -> position -> Reader (Game position) Double
negamax color position
= do state <- getState' position
case state of
FirstPlayerWin -> return color
SecondPlayerWin -> return $ negate color
Tie -> return 0
NotOver -> do possible <- getNext' position
values <- mapM ((liftM negate) . negamax (negate color)) possible
return $ maximum values
Sau đó, điều này sẽ hoạt động với bất kỳ trò chơi hai người chơi hữu hạn, xác định nào.
Mô hình này hữu ích ngay cả đối với những thứ không thực sự là tiêm phụ thuộc. Giả sử bạn làm việc trong lĩnh vực tài chính, bạn có thể thiết kế một số logic phức tạp để định giá một tài sản (ví dụ như phái sinh), điều này hoàn toàn ổn và tốt và bạn có thể làm mà không cần bất kỳ mô hình hôi thối nào. Nhưng sau đó, bạn sửa đổi chương trình của mình để giao dịch với nhiều loại tiền tệ. Bạn cần có thể chuyển đổi nhanh chóng giữa các loại tiền tệ. Nỗ lực đầu tiên của bạn là xác định một hàm cấp cao nhất
type CurrencyDict = Map CurrencyName Dollars
currencyDict :: CurrencyDict
để nhận giá giao ngay. Sau đó, bạn có thể gọi từ điển này trong mã của mình .... nhưng hãy đợi! Điều đó sẽ không hoạt động! Từ điển tiền tệ là bất biến và vì vậy phải giống nhau không chỉ cho vòng đời của chương trình của bạn, mà từ thời điểm nó được biên dịch ! Vậy bạn làm gì? Chà, một tùy chọn sẽ là sử dụng đơn nguyên Reader:
computePrice :: Reader CurrencyDict Dollars
computePrice
= do currencyDict <- ask
--insert computation here
Có lẽ trường hợp sử dụng cổ điển nhất là trong việc triển khai trình thông dịch. Tuy nhiên, trước khi xem xét điều đó, chúng ta cần giới thiệu một chức năng khác
local :: (env -> env) -> Reader env a -> Reader env a
Được rồi, Haskell và các ngôn ngữ hàm khác dựa trên phép tính lambda . Phép tính Lambda có cú pháp giống như
data Term = Apply Term Term | Lambda String Term | Var Term deriving (Show)
và chúng tôi muốn viết một người đánh giá cho ngôn ngữ này. Để làm như vậy, chúng ta sẽ cần theo dõi một môi trường, đó là một danh sách các ràng buộc được liên kết với các điều khoản (thực ra nó sẽ là các bao đóng vì chúng ta muốn thực hiện phạm vi tĩnh).
newtype Env = Env ([(String, Closure)])
type Closure = (Term, Env)
Khi chúng ta hoàn tất, chúng ta sẽ nhận ra một giá trị (hoặc một lỗi):
data Value = Lam String Closure | Failure String
Vì vậy, hãy viết trình thông dịch:
interp' :: Term -> Reader Env Value
--when we have a lambda term, we can just return it
interp' (Lambda nv t)
= do env <- ask
return $ Lam nv (t, env)
--when we run into a value, we look it up in the environment
interp' (Var v)
= do (Env env) <- ask
case lookup (show v) env of
-- if it is not in the environment we have a problem
Nothing -> return . Failure $ "unbound variable: " ++ (show v)
-- if it is in the environment, then we should interpret it
Just (term, env) -> local (const env) $ interp' term
--the complicated case is an application
interp' (Apply t1 t2)
= do v1 <- interp' t1
case v1 of
Failure s -> return (Failure s)
Lam nv clos -> local (\(Env ls) -> Env ((nv, clos) : ls)) $ interp' t2
--I guess not that complicated!
Cuối cùng, chúng ta có thể sử dụng nó bằng cách vượt qua một môi trường tầm thường:
interp :: Term -> Value
interp term = runReader (interp' term) (Env [])
Và đó là nó. Một trình thông dịch đầy đủ chức năng cho phép tính lambda.
Một cách khác để nghĩ về điều này là hỏi: Nó được thực hiện như thế nào? Câu trả lời là đơn nguyên reader thực sự là một trong những đơn nguyên đơn giản và thanh lịch nhất trong số các đơn nguyên.
newtype Reader env a = Reader {runReader :: env -> a}
Reader chỉ là một cái tên ưa thích cho các chức năng! Chúng tôi đã định nghĩa rồi, runReader
vậy còn các phần khác của API thì sao? Vâng, mỗi Monad
cũng là một Functor
:
instance Functor (Reader env) where
fmap f (Reader g) = Reader $ f . g
Bây giờ, để có được một đơn nguyên:
instance Monad (Reader env) where
return x = Reader (\_ -> x)
(Reader f) >>= g = Reader $ \x -> runReader (g (f x)) x
mà không phải là quá đáng sợ. ask
thực sự đơn giản:
ask = Reader $ \x -> x
trong khi local
không quá tệ:
local f (Reader g) = Reader $ \x -> runReader g (f x)
Được rồi, vì vậy đơn nguyên của trình đọc chỉ là một chức năng. Tại sao lại có Reader? Câu hỏi hay. Thực ra, bạn không cần nó!
instance Functor ((->) env) where
fmap = (.)
instance Monad ((->) env) where
return = const
f >>= g = \x -> g (f x) x
Những điều này thậm chí còn đơn giản hơn. Hơn nữa, ask
chỉ là id
và local
chỉ là thành phần chức năng với thứ tự của các chức năng được chuyển đổi!