2013-08-21 8 views
6

W większych aplikacjach bardzo często występuje wiele warstw buforowania IO (Hibernacja L1 i L2, Spring cache itp.), Które zwykle są pobierane, aby dzwoniący nie musiał wiedzieć, że konkretna implementacja wykonuje IO. Dzięki pewnym zastrzeżeniom (zasięg, transakcje) pozwala na łatwiejsze interfejsy między komponentami.Jak wykonać złożone przetwarzanie IO i ukrytą pamięć podręczną w Haskell?

Na przykład, jeśli komponent A potrzebuje do zapytania bazy danych, nie musi wiedzieć, czy wynik jest już buforowany. Mogło zostać odzyskane przez B lub C, o których A nic nie wie, jednak zwykle uczestniczyli w jakiejś sesji lub transakcji - często niejawnie.

Konstrukcje często sprawiają, że wywołanie to jest nie do odróżnienia od wywołania metody obiektowej za pomocą technik takich jak AOP.

Czy aplikacje Haskell mogą z tego korzystać? Jak wyglądałby interfejs klienta?

+1

Możesz zrobić cokolwiek w Haskell, co możesz zrobić w innych językach. Ale typy mogą być inne. – augustss

Odpowiedz

4

W Haskell istnieje wiele sposobów na komponowanie obliczeń z komponentów reprezentujących ich odrębne obowiązki. Można to zrobić na poziomie danych z typami danych i funkcjami (http://www.haskellforall.com/2012/05/scrap-your-type-classes.html) lub używając klas typów. W Haskell można wyświetlić każdy typ danych, typ, funkcję, podpis, klasę itp. Jako interfejs; tak długo, jak masz coś innego tego samego typu, możesz zastąpić komponent czymś, co jest kompatybilne.

Kiedy chcemy wnioskować o obliczeniach w Haskell, często używamy abstrakcji Monad. A Monad to interfejs do konstruowania obliczeń. Obliczenia podstawowe można utworzyć przy pomocy return i można je skomponować razem z funkcjami, które dają inne obliczenia z >>=. Kiedy chcemy dodać wiele obowiązków do obliczeń reprezentowanych przez monady, tworzymy transformatory monad. W poniższym kodzie znajdują się cztery różne transformatory monad, które przechwytują różne aspekty wielowarstwowego systemu:

DatabaseT s dodaje bazę danych ze schematem typu s. Obsługuje dane poprzez przechowywanie danych lub pobieranie ich z bazy danych. CacheT s przechwytuje dane Operation s dla schematu s i pobiera dane z pamięci, jeśli są dostępne. OpperationLoggerT rejestruje Operation s standardowe wyjście ResultLoggerT rejestruje wyniki Operation s standardowe wyjście

Te cztery elementy komunikują się ze sobą przy użyciu klasy typu (interface) nazwie MonadOperation s, która oznacza, że ​​elementy, które nie realizują zapewnić drogę perform an Operation i zwróć wynik.

Ten sam typ klasy opisuje, co jest wymagane do korzystania z systemu MonadOperation s. Wymaga to, aby osoba korzystająca z interfejsu zapewniała implementacje klas typów, na których będzie polegać baza danych i pamięć podręczna. Istnieją również dwa typy danych, które są częścią tego interfejsu, Operation i CRUD. Zauważ, że interfejs nie musi nic wiedzieć o obiektach domenowych lub schemacie bazy danych, ani nie musi wiedzieć o różnych transformatorach monad, które go wdrożą. Transformatory monad nie wiedzą nic o schemacie ani obiektach domenowych, a obiekty domeny i przykładowy kod nie wiedzą nic o transformatorach monad, które budują system.

Kod przykładowy wie tylko, że będzie miał dostęp do MonadOperation s ze względu na typ: example :: (MonadOperation TableName m) => m().

Program main uruchamia przykład dwa razy w dwóch różnych kontekstach. Za pierwszym razem program komunikuje się z bazą danych, pod numerem Operations, a odpowiedzi są rejestrowane w standardzie.

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."}) 

Drugi bieg rejestruje reakcje program przyjmuje, przekazuje Operation s za pośrednictwem pamięci podręcznej, i rejestruje wnioski, zanim dotrą do bazy. Ze względu na nowe buforowania, który jest przezroczysty dla programu, wnioski, aby przeczytać artykuł nigdy nie nastąpi, ale program nadal otrzymuje odpowiedź:

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."}) 

Oto cały kod źródłowy. Powinieneś myśleć o tym jako o czterech niezależnych fragmentach kodu: Program napisany dla naszej domeny, zaczynając od example. Aplikacja, która jest kompletnym zbiorem programu, domeny dyskursu i różnych narzędzi, które go tworzą, począwszy od main. Następne dwie sekcje, kończące się schematem TableName, opisują domenę postów na blogu; ich jedynym celem jest zilustrowanie, w jaki sposób inne komponenty idą w parze, a nie służyć jako przykład do projektowania struktur danych w Haskell. W następnej sekcji opisano mały interfejs, za pomocą którego komponenty mogą komunikować się o danych; to niekoniecznie dobry interfejs. Na koniec, pozostała część kodu źródłowego implementuje rejestratory, bazę danych i pamięci podręczne, które składają się razem w celu utworzenia aplikacji. Aby oddzielić narzędzia i interfejs od domeny, istnieją tu nieco obrzydliwe sztuczki z możliwymi do opisania i dynamiki tutaj, to nie jest po to, aby pokazać dobry sposób na obsłużenie odlewu i generycznych.

{-# 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 

Aby zbudować ten przykład, trzeba bibliotek mtl i containers.

+1

Dziękuję za tak wyczerpujące wyjaśnienie. Ten kod pokazuje, że można wyświetlić zaskakująco przyjazny dla użytkownika i bezpieczny interfejs typu. Liczba używanych rozszerzeń jest dla mnie otwieraczką do oczu, ponieważ podejrzewałem, że standardowy Haskell zmusi programistę do radzenia sobie z wieloma typami kotłów. – Rumca

3

W Haskell, musisz do musisz (i chcesz!) Być świadomym wszystkiego, co robi IO.

To jedna z mocnych stron na ten temat.

Można użyć klasy MonadIO typu napisać funkcje, które działają w każdej monady, który jest dozwolony do wykonywania czynności IO:

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

Jak wielu interfejsów programowania w Haskell są wyrażone poprzez monad, funkcje takie jak to może działać w większej liczbie kontekstów.

Możesz także użyć unsafePerformIO, aby potajemnie uruchomić działania IO z czystego kodu - jednak nie jest to zalecane w prawie wszystkich przypadkach. Bycie czystym pozwala natychmiast zobaczyć, czy efekty uboczne są używane, czy nie.

Buforowanie IO jest efektem ubocznym, a Ty masz się dobrze, jeśli twoje typy to odzwierciedlają.

Powiązane problemy