Как выполнить сложную обработку ввода-вывода и неявный кеш в Haskell?

В больших приложениях часто используется несколько уровней кэширования IO (Hibernate L1 и L2, Spring и т.д.), которые обычно абстрагируются, поэтому вызывающему абоненту не нужно знать, что конкретная реализация делает IO. С некоторыми оговорками (объем, транзакции) он позволяет упростить интерфейс между компонентами.

Например, если компоненту A необходимо запросить базу данных, ему не нужно знать, будет ли результат уже кэширован. Возможно, он был найден B или C, о которых A ничего не знает, однако они обычно участвуют в некоторых сеансах или транзакциях - часто неявно.

Рамки, как правило, делают этот вызов неотличимым от простого вызова метода объекта с использованием таких методов, как AOP.

Возможно ли, чтобы приложения Haskell получили преимущества? Как выглядит интерфейс клиента?

2 ответа

В Haskell существует множество способов составления вычислений из компонентов, которые представляют собой их отдельные обязанности. Это можно сделать на уровне данных с типами данных и функциями (http://www.haskellforall.com/2012/05/scrap-your-type-classes.html) или с использованием классов типов. В Haskell вы можете просматривать каждый тип данных, тип, функцию, подпись, класс и т.д. В качестве интерфейса; если у вас есть что-то еще одного типа, вы можете заменить компонент чем-то совместимым.

Когда мы хотим рассуждать о вычислениях в Haskell, мы часто используем абстракцию a Monad. A Monad - это интерфейс для построения вычислений. Базовое вычисление можно построить с помощью return, и они могут быть составлены вместе с функциями, которые производят другие вычисления с помощью >>=. Когда мы хотим добавить несколько обязанностей к вычислениям, представленным монадами, мы делаем монадные трансформаторы. В приведенном ниже коде есть четыре разных монадных трансформатора, которые захватывают различные аспекты многоуровневой системы:

DatabaseT s добавляет базу данных со схемой типа s. Он обрабатывает данные Operation, сохраняя данные или извлекая их из базы данных. CacheT s перехватывает данные Operation для схемы s и извлекает данные из памяти, если она доступна. OpperationLoggerT записывает Operation в стандартный вывод ResultLoggerT записывает результаты Operation в стандартный вывод

Эти четыре компонента объединяются вместе с использованием класса (интерфейса) типа MonadOperation s, который требует, чтобы реализующие его компоненты обеспечивали способ perform a Operation и возвращали его результат.

В этом же типе описывается то, что требуется для использования системы MonadOperation s. Это требует, чтобы кто-то, использующий интерфейс, обеспечивал реализацию классов типов, на которые будут основываться база данных и кеш. Существуют также два типа данных, которые являются частью этого интерфейса, Operation и CRUD. Обратите внимание, что интерфейс не должен ничего знать о объектах домена или схеме базы данных, а также не должен знать о разных монадных трансформаторах, которые будут его реализовывать. Монадные трансформаторы ничего не знают о объектах схемы или домена, а объекты домена и пример кода ничего не знают о монадных трансформаторах, которые строят систему.

Единственное, что знает код примера, это доступ к MonadOperation s из-за его типа example :: (MonadOperation TableName m) => m ().

Программа main дважды запускает пример в двух разных контекстах. В первый раз программа разговаривает с базой данных, ее Operations и ответы записываются в стандартную версию.

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

Второй запуск регистрирует ответы, полученные программой, передает Operation через кеш и регистрирует запросы до того, как они достигнут базы данных. Из-за нового кэширования, прозрачного для программы, запросы на чтение статьи никогда не происходят, но программа все равно получает ответ:

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

Здесь весь исходный код. Вы должны думать об этом как о четырех независимых фрагментах кода: программа, написанная для нашего домена, начиная с example. Приложение, которое представляет собой полную сборку программы, область дискурса и различные инструменты, которые ее создают, начиная с main. Следующие два раздела, заканчивающиеся схемой TableName, описывают домен сообщений в блогах; их единственная цель - проиллюстрировать, как другие компоненты объединяются, а не служить примером проектирования структур данных в Haskell. В следующем разделе описывается небольшой интерфейс, посредством которого компоненты могут связываться с данными; это не обязательно хороший интерфейс. Наконец, оставшаяся часть исходного кода реализует журналы, базу данных и кеши, которые составлены вместе для формирования приложения. Чтобы отделить инструменты и интерфейс от домена, есть несколько отвратительных трюков с типом и динамикой здесь, это не означает, что можно продемонстрировать хороший способ обработки кастингов и дженериков.

{-# LANGUAGE ******************, GADTs, DeriveDataTypeable, FlexibleInstances, FlexibleContexts, **************************, 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 -> ****** 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 op@(Operation _ (Create value)) = do
 key <- perform op
 modify (Map.insert key value)
 return key
cacheMapOperation op@(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 op@(Operation _ (List)) = do
 values <- perform op
 modify (Map.union (Map.fromList values))
 current <- get
 return (Map.toList current)
cacheMapOperation op@(Operation _ (Update key value)) = do
 successful <- perform op
 modify (Map.update (\_ -> (successful >>= (\_ -> Just value))) key)
 return successful
cacheMapOperation op@(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 op@(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

Чтобы создать этот пример, вам понадобятся библиотеки mtl и containers.


В Haskell вы do должны (и хотите!) быть в курсе всего, что делает IO.

Это одна из сильных сторон.

Вы можете использовать класс типа MonadIO для записи функций, которые работают в любой монаде, которой разрешено выполнять операции ввода-вывода:

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

Поскольку многие интерфейсы программирования в Haskell выражаются через монады, такие функции могут работать в большем количестве контекстов.

Вы также можете использовать unsafePerformIO для тайного запуска операций ввода-вывода из чистого кода, однако это практически не рекомендуется практически во всех случаях. Быть чистым позволяет сразу увидеть, используются ли побочные эффекты или нет.

IO-кеширование - побочный эффект, и вы хорошо себя чувствуете, если ваши типы отражают это.

licensed under cc by-sa 3.0 with attribution.