Trong Haskell, có nhiều cách để tính toán các thành phần đại diện cho các trách nhiệm riêng biệt của chúng. Điều này có thể được thực hiện ở cấp dữ liệu với các kiểu dữ liệu và chức năng (http://www.haskellforall.com/2012/05/scrap-your-type-classes.html) hoặc sử dụng các loại lớp. Trong Haskell bạn có thể xem mọi kiểu dữ liệu, kiểu, hàm, chữ ký, lớp, vv như một giao diện; miễn là bạn có thứ gì đó khác cùng loại, bạn có thể thay thế một thành phần bằng thứ gì đó tương thích.
Khi chúng tôi muốn lý do về tính toán trong Haskell, chúng tôi thường sử dụng sự trừu tượng hóa của Monad
. A Monad
là một giao diện để xây dựng tính toán. Một tính toán cơ bản có thể được xây dựng với return
và chúng có thể được tạo thành cùng với các hàm tạo ra các tính toán khác với >>=
. Khi chúng ta muốn thêm nhiều trách nhiệm vào các tính toán được đại diện bởi các đơn nguyên, chúng ta tạo ra các biến thế đơn nguyên. Trong mã bên dưới, có bốn biến thế đơn nguyên khác nhau nắm bắt các khía cạnh khác nhau của hệ thống phân lớp:
DatabaseT s
thêm cơ sở dữ liệu với lược đồ loại s
. Nó xử lý dữ liệu Operation
s bằng cách lưu trữ dữ liệu hoặc lấy dữ liệu từ cơ sở dữ liệu. CacheT s
chặn dữ liệu Operation
s cho lược đồ s
và truy xuất dữ liệu từ bộ nhớ, nếu có. OpperationLoggerT
ghi lại Operation
s để đầu ra tiêu chuẩn ResultLoggerT
ghi lại kết quả của Operation
s để đầu ra tiêu chuẩn
Bốn thành phần giao tiếp với nhau bằng cách sử dụng kiểu lớp (giao diện) gọi MonadOperation s
, đòi hỏi rằng các thành phần mà thực hiện nó cung cấp một cách để perform
an Operation
và trả về kết quả của nó.
Lớp học cùng loại này mô tả những gì được yêu cầu để sử dụng hệ thống MonadOperation s
. Nó yêu cầu ai đó sử dụng giao diện cung cấp việc triển khai các lớp kiểu mà cơ sở dữ liệu và bộ nhớ cache sẽ dựa vào. Ngoài ra còn có hai loại dữ liệu là một phần của giao diện này, Operation
và CRUD
. Lưu ý rằng giao diện không cần phải biết bất cứ điều gì về các đối tượng miền hoặc lược đồ cơ sở dữ liệu, cũng không cần biết về các máy biến áp đơn nguyên khác nhau sẽ thực hiện nó. Biến thế đơn nguyên không biết gì về lược đồ hoặc đối tượng miền, và các đối tượng miền và mã ví dụ không biết gì về các biến thế đơn lẻ xây dựng hệ thống.
Điều duy nhất mà mã ví dụ biết là nó sẽ có quyền truy cập vào số MonadOperation s
do loại example :: (MonadOperation TableName m) => m()
.
Chương trình main
chạy ví dụ hai lần trong hai ngữ cảnh khác nhau. Lần đầu tiên, chương trình nói chuyện với cơ sở dữ liệu, với số Operations
và các câu trả lời được ghi vào tiêu chuẩn.
Running example program once with an empty database
Operation Articles (Create (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."}))
ArticleId 0
Operation Articles (Read (ArticleId 0))
Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
Operation Articles (Read (ArticleId 0))
Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
Việc chạy thứ hai ghi lại phản ứng của chương trình nhận được, đi Operation
s thông qua bộ nhớ cache, và ghi lại những yêu cầu trước khi chúng đạt đến cơ sở dữ liệu. Do bộ nhớ đệm mới, minh bạch cho chương trình, yêu cầu đọc bài viết không bao giờ xảy ra, nhưng chương trình vẫn nhận được phản hồi:
Running example program once with an empty cache and an empty database
Operation Articles (Create (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."}))
ArticleId 0
Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
Just (Article {title = "My first article", author = "Cirdec", contents = "Lorem ipsum dolor sit amet."})
Đây là toàn bộ mã nguồn. Bạn nên nghĩ về nó như là bốn phần mã độc lập: Một chương trình được viết cho miền của chúng tôi, bắt đầu từ example
. Một ứng dụng là tập hợp đầy đủ của chương trình, tên miền của diễn ngôn và các công cụ khác nhau để xây dựng nó, bắt đầu từ main
. Hai phần tiếp theo, kết thúc bằng lược đồ TableName
, mô tả tên miền của các bài đăng trên blog; mục đích duy nhất của họ là minh họa cách các thành phần khác đi cùng nhau, không phải là một ví dụ về cách thiết kế cấu trúc dữ liệu trong Haskell. Phần tiếp theo mô tả một giao diện nhỏ mà các thành phần có thể giao tiếp về dữ liệu; nó không nhất thiết phải là một giao diện tốt. Cuối cùng, phần còn lại của mã nguồn thực hiện các logger, cơ sở dữ liệu và các cache được tạo thành cùng nhau để tạo thành ứng dụng. Để tách rời các công cụ và giao diện khỏi miền, có một số thủ thuật hơi ghê tởm với khả năng đánh máy và động lực ở đây, điều này không có nghĩa là để chứng minh một cách tốt để xử lý đúc và Generics.
{-# LANGUAGE StandaloneDeriving, GADTs, DeriveDataTypeable, FlexibleInstances, FlexibleContexts, GeneralizedNewtypeDeriving, MultiParamTypeClasses, ScopedTypeVariables, KindSignatures, FunctionalDependencies, UndecidableInstances #-}
module Main (
main
) where
import Data.Typeable
import qualified Data.Map as Map
import Control.Monad.State
import Control.Monad.State.Class
import Control.Monad.Trans
import Data.Dynamic
-- Example
example :: (MonadOperation TableName m) => m()
example =
do
id <- perform $ Operation Articles $ Create $ Article {
title = "My first article",
author = "Cirdec",
contents = "Lorem ipsum dolor sit amet."
}
perform $ Operation Articles $ Read id
perform $ Operation Articles $ Read id
cid <- perform $ Operation Comments $ Create $ Comment {
article = id,
user = "Cirdec",
comment = "Commenting on my own article!"
}
perform $ Operation Equality $ Create False
perform $ Operation Equality $ Create True
perform $ Operation Inequality $ Create True
perform $ Operation Inequality $ Create False
perform $ Operation Articles $ List
perform $ Operation Comments $ List
perform $ Operation Equality $ List
perform $ Operation Inequality $ List
return()
-- Run the example twice, changing the cache transparently to the code
main :: IO()
main = do
putStrLn "Running example program once with an empty database"
runDatabaseT (runOpperationLoggerT (runResultLoggerT example)) Types { types = Map.empty }
putStrLn "\nRunning example program once with an empty cache and an empty database"
runDatabaseT (runOpperationLoggerT (runCacheT (runResultLoggerT example) Types { types = Map.empty })) Types { types = Map.empty }
return()
-- Domain objects
data Article = Article {
title :: String,
author :: String,
contents :: String
}
deriving instance Eq Article
deriving instance Ord Article
deriving instance Show Article
deriving instance Typeable Article
newtype ArticleId = ArticleId Int
deriving instance Eq ArticleId
deriving instance Ord ArticleId
deriving instance Show ArticleId
deriving instance Typeable ArticleId
deriving instance Enum ArticleId
data Comment = Comment {
article :: ArticleId,
user :: String,
comment :: String
}
deriving instance Eq Comment
deriving instance Ord Comment
deriving instance Show Comment
deriving instance Typeable Comment
newtype CommentId = CommentId Int
deriving instance Eq CommentId
deriving instance Ord CommentId
deriving instance Show CommentId
deriving instance Typeable CommentId
deriving instance Enum CommentId
-- Database Schema
data TableName k v where
Articles :: TableName ArticleId Article
Comments :: TableName CommentId Comment
Equality :: TableName Bool Bool
Inequality :: TableName Bool Bool
deriving instance Eq (TableName k v)
deriving instance Ord (TableName k v)
deriving instance Show (TableName k v)
deriving instance Typeable2 TableName
-- Data interface (Persistance library types)
data CRUD k v r where
Create :: v -> CRUD k v k
Read :: k -> CRUD k v (Maybe v)
List :: CRUD k v [(k,v)]
Update :: k -> v -> CRUD k v (Maybe())
Delete :: k -> CRUD k v (Maybe())
deriving instance (Eq k, Eq v) => Eq (CRUD k v r)
deriving instance (Ord k, Ord v) => Ord (CRUD k v r)
deriving instance (Show k, Show v) => Show (CRUD k v r)
data Operation s t k v r where
Operation :: t ~ s k v => t -> CRUD k v r -> Operation s t k v r
deriving instance (Eq (s k v), Eq k, Eq v) => Eq (Operation s t k v r)
deriving instance (Ord (s k v), Ord k, Ord v) => Ord (Operation s t k v r)
deriving instance (Show (s k v), Show k, Show v) => Show (Operation s t k v r)
class (Monad m) => MonadOperation s m | m -> s where
perform :: (Typeable2 s, Typeable k, Typeable v, t ~ s k v, Show t, Ord v, Ord k, Enum k, Show k, Show v, Show r) => Operation s t k v r -> m r
-- Database implementation
data Tables t k v = Tables {
tables :: Map.Map String (Map.Map k v)
}
deriving instance Typeable3 Tables
emptyTablesFor :: Operation s t k v r -> Tables t k v
emptyTablesFor _ = Tables {tables = Map.empty}
data Types = Types {
types :: Map.Map TypeRep Dynamic
}
-- Database emulator
mapOperation :: (Enum k, Ord k, MonadState (Map.Map k v) m) => (CRUD k v r) -> m r
mapOperation (Create value) = do
current <- get
let id = case Map.null current of
True -> toEnum 0
_ -> succ maxId where
(maxId, _) = Map.findMax current
put (Map.insert id value current)
return id
mapOperation (Read key) = do
current <- get
return (Map.lookup key current)
mapOperation List = do
current <- get
return (Map.toList current)
mapOperation (Update key value) = do
current <- get
case (Map.member key current) of
True -> do
put (Map.update (\_ -> Just value) key current)
return (Just())
_ -> return Nothing
mapOperation (Delete key) = do
current <- get
case (Map.member key current) of
True -> do
put (Map.delete key current)
return (Just())
_ -> return Nothing
tableOperation :: (Enum k, Ord k, Ord v, t ~ s k v, Show t, MonadState (Tables t k v) m) => Operation s t k v r -> m r
tableOperation (Operation tableName op) = do
current <- get
let currentTables = tables current
let tableKey = show tableName
let table = Map.findWithDefault (Map.empty) tableKey currentTables
let (result,newState) = runState (mapOperation op) table
put Tables { tables = Map.insert tableKey newState currentTables }
return result
typeOperation :: (Enum k, Ord k, Ord v, t ~ s k v, Show t, Typeable2 s, Typeable k, Typeable v, MonadState Types m) => Operation s t k v r -> m r
typeOperation op = do
current <- get
let currentTypes = types current
let empty = emptyTablesFor op
let typeKey = typeOf (empty)
let typeMap = fromDyn (Map.findWithDefault (toDyn empty) typeKey currentTypes) empty
let (result, newState) = runState (tableOperation op) typeMap
put Types { types = Map.insert typeKey (toDyn newState) currentTypes }
return result
-- Database monad transformer (clone of StateT)
newtype DatabaseT (s :: * -> * -> *) m a = DatabaseT {
databaseStateT :: StateT Types m a
}
runDatabaseT :: DatabaseT s m a -> Types -> m (a, Types)
runDatabaseT = runStateT . databaseStateT
instance (Monad m) => Monad (DatabaseT s m) where
return = DatabaseT . return
(DatabaseT m) >>= k = DatabaseT (m >>= \x -> databaseStateT (k x))
instance MonadTrans (DatabaseT s) where
lift = DatabaseT . lift
instance (MonadIO m) => MonadIO (DatabaseT s m) where
liftIO = DatabaseT . liftIO
instance (Monad m) => MonadOperation s (DatabaseT s m) where
perform = DatabaseT . typeOperation
-- State monad transformer can preserve operations
instance (MonadOperation s m) => MonadOperation s (StateT state m) where
perform = lift . perform
-- Cache implementation (very similar to emulated database)
cacheMapOperation :: (Enum k, Ord k, Ord v, t ~ s k v, Show t, Show k, Show v, Typeable2 s, Typeable k, Typeable v, MonadState (Map.Map k v) m, MonadOperation s m) => Operation s t k v r -> m r
cacheMapOperation [email protected](Operation _ (Create value)) = do
key <- perform op
modify (Map.insert key value)
return key
cacheMapOperation [email protected](Operation _ (Read key)) = do
current <- get
case (Map.lookup key current) of
Just value -> return (Just value)
_ -> do
value <- perform op
modify (Map.update (\_ -> value) key)
return value
cacheMapOperation [email protected](Operation _ (List)) = do
values <- perform op
modify (Map.union (Map.fromList values))
current <- get
return (Map.toList current)
cacheMapOperation [email protected](Operation _ (Update key value)) = do
successful <- perform op
modify (Map.update (\_ -> (successful >>= (\_ -> Just value))) key)
return successful
cacheMapOperation [email protected](Operation _ (Delete key)) = do
result <- perform op
modify (Map.delete key)
return result
cacheTableOperation :: (Enum k, Ord k, Ord v, t ~ s k v, Show t, Show k, Show v, Typeable2 s, Typeable k, Typeable v, MonadState (Tables t k v) m, MonadOperation s m) => Operation s t k v r -> m r
cacheTableOperation [email protected](Operation tableName _) = do
current <- get
let currentTables = tables current
let tableKey = show tableName
let table = Map.findWithDefault (Map.empty) tableKey currentTables
(result,newState) <- runStateT (cacheMapOperation op) table
put Tables { tables = Map.insert tableKey newState currentTables }
return result
cacheTypeOperation :: (Enum k, Ord k, Ord v, t ~ s k v, Show t, Show k, Show v, Typeable2 s, Typeable k, Typeable v, MonadState Types m, MonadOperation s m) => Operation s t k v r -> m r
cacheTypeOperation op = do
current <- get
let currentTypes = types current
let empty = emptyTablesFor op
let typeKey = typeOf (empty)
let typeMap = fromDyn (Map.findWithDefault (toDyn empty) typeKey currentTypes) empty
(result, newState) <- runStateT (cacheTableOperation op) typeMap
put Types { types = Map.insert typeKey (toDyn newState) currentTypes }
return result
-- Cache monad transformer
newtype CacheT (s :: * -> * -> *) m a = CacheT {
cacheStateT :: StateT Types m a
}
runCacheT :: CacheT s m a -> Types -> m (a, Types)
runCacheT = runStateT . cacheStateT
instance (Monad m) => Monad (CacheT s m) where
return = CacheT . return
(CacheT m) >>= k = CacheT (m >>= \x -> cacheStateT (k x))
instance MonadTrans (CacheT s) where
lift = CacheT . lift
instance (MonadIO m) => MonadIO (CacheT s m) where
liftIO = CacheT . liftIO
instance (Monad m, MonadOperation s m) => MonadOperation s (CacheT s m) where
perform = CacheT . cacheTypeOperation
-- Logger monad transform
newtype OpperationLoggerT m a = OpperationLoggerT {
runOpperationLoggerT :: m a
}
instance (Monad m) => Monad (OpperationLoggerT m) where
return = OpperationLoggerT . return
(OpperationLoggerT m) >>= k = OpperationLoggerT (m >>= \x -> runOpperationLoggerT (k x))
instance MonadTrans (OpperationLoggerT) where
lift = OpperationLoggerT
instance (MonadIO m) => MonadIO (OpperationLoggerT m) where
liftIO = OpperationLoggerT . liftIO
instance (MonadOperation s m, MonadIO m) => MonadOperation s (OpperationLoggerT m) where
perform op = do
liftIO $ putStrLn $ show op
lift (perform op)
-- Result logger
newtype ResultLoggerT m a = ResultLoggerT {
runResultLoggerT :: m a
}
instance (Monad m) => Monad (ResultLoggerT m) where
return = ResultLoggerT . return
(ResultLoggerT m) >>= k = ResultLoggerT (m >>= \x -> runResultLoggerT (k x))
instance MonadTrans (ResultLoggerT) where
lift = ResultLoggerT
instance (MonadIO m) => MonadIO (ResultLoggerT m) where
liftIO = ResultLoggerT . liftIO
instance (MonadOperation s m, MonadIO m) => MonadOperation s (ResultLoggerT m) where
perform op = do
result <- lift (perform op)
liftIO $ putStrLn $ "\t" ++ (show result)
return result
Để tạo ví dụ này, bạn sẽ cần thư viện mtl
và containers
.
Bạn có thể làm bất cứ điều gì trong Haskell mà bạn có thể làm bằng các ngôn ngữ khác. Nhưng các loại có thể khác nhau. – augustss