1

(Related question Select instance behavior at runtime)

I want to define a kind of backend (reusable as independent api), then provide multiple implementations and be able to select one at runtime.

My application will be like

data AppData =
     AppData { ...
             , backend :: Backend
             }

for simplicity, let our backend (conceptual code)

data Backend key value =
     Backend { get :: [(key, value)]
             , cud :: key -> Maybe value -> () -- Create Update Delete
             }

now What's the proper/recommended way to define our Backend type?

I think it will be monadic (then Monad m) but also IO (then MonadIO m) but also pure (then we need change -> () by -> Backend key value) and so on...

I'm doubting, the next attempt works monadic, IO and pure, but may be over-engineering

data Backend m k v =
     Backend { get :: MonadIO m => m [(k, v)]
             , cud :: MonadIO m => k -> Maybe v -> Backend m k v
             }

the MonadIO is a strong restriction and return immutable version on cud is redundant (in most cases?) with m.

What is the proper/recommended way to abstract it?

Thank you!

Once defined our Backend API my intention was use as (conceptual code) more or less

main = do
    configuration <- getAppConfiguration
    selectedBackend <- case preferedBackend configuration of
                           "mongoDB"  -> MongoDBBackend.makeBackend
                           "sqlite"   -> SqliteBackend.makeBackend
                           "volatile" -> PureDataMapBackend.makeBackend
                           ...
    appData <- makeAppData configuration selectedBackend
    ....
Community
  • 1
  • 1
josejuan
  • 9,338
  • 24
  • 31

1 Answers1

7

If you need a non-IO backend as well, then I'd suggest to parametrize the data type by the monad in which its operations run. As pointed out by @Cactus, there is no need to add constraints to the data type itself. It doesn't help anything, it just makes things complicated. Instead, these constraints will be at functions that create various Backends.

Also, while it might be possible to vary the return type of the update function, using such a function would be just hell, as the application would basically need to cover all (both) cases whenever using the function. So instead I'd suggest to keep the result simple, just monadic. And for the pure backend you can just run the application within the State monad, for example:

{-# LANGUAGE FlexibleContexts #-}
import Control.Monad.State

data Backend m key value = Backend
    { beGet :: m [(key, value)]
    , beCud :: key -> Maybe value -> m () -- Create Update Delete
    }

pureBackend :: (MonadState [(k, v)] m, Eq k) => Backend m k v
pureBackend = Backend get pureCud
  where
    filterOut k = filter ((/= k) . fst)
    pureCud k Nothing = modify (filterOut k)
    pureCud k (Just v) = modify (((k, v) :) . filterOut k)

-- other backends ...

This means that AppData and whoever else uses Backend needs to be parametrized by the monad as well.

Petr
  • 62,528
  • 13
  • 153
  • 317