2013-08-21 37 views
6

Trong các ứng dụng lớn hơn thường có nhiều lớp bộ nhớ đệm IO (Hibernate L1 và L2, bộ nhớ đệm mùa xuân, v.v.) thường được trừu tượng để người gọi không cần phải biết rằng việc triển khai thực hiện cụ thể là IO. Với một số cảnh báo (phạm vi, giao dịch), nó cho phép các giao diện đơn giản hơn giữa các thành phần.Làm thế nào để xử lý IO phức tạp và bộ nhớ cache ẩn trong Haskell?

Ví dụ, nếu thành phần A cần truy vấn cơ sở dữ liệu, nó không cần phải biết liệu kết quả đã được lưu trữ chưa. Nó có thể đã được lấy bởi B hoặc C mà A không biết gì về, tuy nhiên họ thường sẽ tham gia vào một số phiên hoặc giao dịch - thường ngầm.

Khung có xu hướng thực hiện cuộc gọi này không thể phân biệt được từ lời gọi phương thức đối tượng đơn giản bằng các kỹ thuật như AOP.

Ứng dụng Haskell có thể hưởng lợi như thế này không? Giao diện của khách hàng sẽ trông như thế nào?

+1

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

Trả lời

4

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, OperationCRUD. 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 mtlcontainers.

+1

Cảm ơn bạn đã giải thích đầy đủ như vậy. Mã này cho thấy rằng có thể hiển thị giao diện an toàn và thân thiện với người dùng đáng ngạc nhiên. Số lượng các phần mở rộng được sử dụng là một mở mắt cho tôi như tôi nghi ngờ tiêu chuẩn Haskell sẽ buộc lập trình để đối phó với rất nhiều gõ boilerplatte. – Rumca

3

Trong Haskell, bạn do cần phải (và muốn!) Nhận thức được bất kỳ điều gì làm IO.

Đó là một trong những điểm mạnh về nó.

Bạn có thể sử dụng lớp MonadIO loại để viết chức năng mà làm việc trong bất kỳ đơn nguyên được phép thực hiện hành động IO:

myFunctionUsingIO :: (MonadIO m) => ... -> m someReturntype 
myFunctionUsingIO = do 
    -- some code 
    liftIO $ ... -- some IO code 
    -- some other code 

Như nhiều giao diện lập trình trong Haskell được thể hiện qua monads, chức năng như thế này có thể làm việc trong nhiều ngữ cảnh hơn.

Bạn cũng có thể sử dụng unsafePerformIO để bí mật chạy các tác vụ IO từ mã thuần túy - tuy nhiên điều này không được khuyến khích trong hầu hết các trường hợp. Tinh khiết cho phép bạn ngay lập tức xem liệu tác dụng phụ có được sử dụng hay không.

Lưu vào bộ nhớ đệm IO là một tác dụng phụ và bạn sẽ tắt nếu các loại phản ánh điều đó.

Các vấn đề liên quan