Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

In Haskell is dependency injection using ExistentialQuantification an anti-pattern?

I am a Haskell newbie, and I am thinking about how I can modularize my Rest application, which essentially passes around a ReaderT everywhere. I have devised a primitive working example of how to do that (below) using ExistentialQuantification. In a comment to a relevant answer, user MathematicalOrchid claimed something similar to be an anti-pattern. Is this an anti-pattern? In newbie terms, can you explain why if so and show a better alternative?

{-# LANGUAGE ExistentialQuantification #-}

import Control.Monad.Reader
import Control.Monad.Trans
import Data.List (intersect)

data Config = Config Int Bool


data User = Jane | John | Robot deriving (Show)
listUsers = [Jane, John, Robot]

class Database d where
  search :: d -> String -> IO [User]
  fetch  :: d -> Int -> IO (Maybe User)


data LiveDb = LiveDb
instance Database LiveDb where
  search d q   = return $ filter ((q==) . intersect q . show) listUsers
  fetch d i = return $ if i<3  then Just $ listUsers!!i else Nothing

data TestDb = TestDb
instance Database TestDb where
  search _ _ = return [Robot]
  fetch _ _ = return $ Just Robot

data Context = forall d. (Database d) => Context {
    db :: d
  , config :: Config
  }

liveContext = Context { db = LiveDb, config = Config 123 True }
testContext = Context { db = TestDb, config = Config 123 True }

runApi :: String -> ReaderT Context IO String
runApi query = do  
  Context { db = db } <- ask
  liftIO . fmap show $ search db query

main = do
  let q = "Jn"

  putStrLn $ "searching users for " ++ q

  liveResult <- runReaderT (runApi q) liveContext
  putStrLn $ "live result " ++ liveResult

  testResult <- runReaderT (runApi q) testContext
  putStrLn $ "test result " ++ testResult

Edit: a working example based on the accepted answer

import Control.Monad.Reader
import Control.Monad.Trans
import Data.List (intersect)

data Config = Config Int Bool


data User = Jane | John | Robot deriving (Show)
listUsers = [Jane, John, Robot]

data Database = Database {
    search :: String -> IO [User]
  , fetch  :: Int -> IO (Maybe User)
  }


liveDb :: Database
liveDb = Database search fetch where
  search q = return $ filter ((q==) . intersect q . show) listUsers
  fetch i = return $ if i<3  then Just $ listUsers!!i else Nothing

testDb :: Database
testDb = Database search fetch where
  search _ = return [Robot]
  fetch  _ = return $ Just Robot

data Context = Context {
    db :: Database
  , config :: Config
  }

liveContext = Context { db = liveDb, config = Config 123 True }
testContext = Context { db = testDb, config = Config 123 True }

runApi :: String -> ReaderT Context IO String
runApi query = do  
  d <- fmap db $ ask
  liftIO . fmap show $ search d $ query

main = do
  let q = "Jn"

  putStrLn $ "searching users for " ++ q

  liveResult <- runReaderT (runApi q) liveContext
  putStrLn $ "live result " ++ liveResult

  testResult <- runReaderT (runApi q) testContext
  putStrLn $ "test result " ++ testResult
like image 837
Bijou Trouvaille Avatar asked Jul 23 '15 02:07

Bijou Trouvaille


2 Answers

The argument against existential types is quite simple (and strong): often, you can avoid both the existential type and type class machinery, and use plain functions instead.

This is clearly the case where your class has the form

class D a where
   method1 :: a -> T1
   method2 :: a -> T2
   -- ...

as in the posted Database example, since its instances can be replaced by values in a plain record type

data D = {
   method1 :: T1
,  method2 :: T2
   -- ...
}

This is, essentially, the solution by @LuisCasillas .

However, note that the above translation relies on types T1,T2 not to depend on a. What if this is not the case? E.g. what if we had

class Database d where
  search :: d -> String -> [User]
  fetch  :: d -> Int -> Maybe User
  insert :: d -> User -> d

The above is a "pure" (no-IO) interface to a database, also allowing updates through insert. An instance could then be

data LiveDb = LiveDb [User]
instance Database LiveDb where
   search (LiveDb d) q = filter ((q==) . intersect q . show) d
   fetch  (LiveDb d) i = case drop i d of [] -> Nothing ; (x:_) -> Just x
   insert (LiveDb d) u = LiveDb (u:d)

Note that here we do use the parameter d, unlike in the original case where it was a placeholder.

Can we do without classes and existentials here?

data Database =
  Database { search :: String -> [User]
           , fetch  :: Int -> Maybe User
           , insert :: User -> Database
           }

Notice that above we are returning an abstract Database in insert. This interface is more general than the existential-classy one, since it allows insert to change the underlying representation for the database. I.e., insert could move from a list-based representation to a tree-based one. This is like having insert acting from the existentially-quantified Database to itself, instead of from a concrete instance to itself.

Anyway, let's write LiveDb in the record-style way:

liveDb :: Database
liveDb = Database (search' listUsers) (fetch' listUsers) (insert' listUsers)
  where search' d q = filter ((q==) . intersect q . show) d
        fetch' d i  = case drop i d of [] -> Nothing ; (x:_) -> Just x
        insert' d u = Database (search' d') (fetch' d') (insert' d')
              where d' = u:d
        listUsers = [Jane, John, Robot]

Above I had to pass the underlying state d to each function, and in insert I had to update such state.

Overall, I find the above more involved than the instance Database LiveDb methods, which require no state-passing. Surely, we can apply a little refactoring and clarify the code:

makeLiveDb :: [User] -> Database
makeLiveDb d = Database search fetch insert
   where search q = filter ((q==) . intersect q . show) d
         fetch i  = case drop i d of [] -> Nothing ; (x:_) -> Just x
         insert u = makeLiveDb (u:d)

liveDb :: Database
liveDb = makeLiveDb [Jane, John, Robot]

This is a bit better, yet not as simple than the plain instance. There is no straightforward winner in this case, and which style to use is a matter of personal preference.

Personally, I stay away from existentially-quantified classes as much as possible, since in many, many cases they lose to much simpler approaches. However, I'm not dogmatic about them, and allow myself to use the "anti-pattern" when the alternative starts becoming too clumsy.


As an alternative, one could use an external function working at the abstract level, only:

data Database =
  Database { search :: String -> [User]
           -- let's neglect other methods for simplicity's sake
           }

insert :: Database -> User -> Database
insert (Database s) u = Database s'
   where s' str = s str ++ [ u | show u == str ] -- or something similar

The advantage of doing this is that insert works on the abstract Database, whatever its underlying data structure is. The disadvantage is that, in this way, insert can only access the database through its "methods", and can only work by building closures upon closures. If we also implemented a remove method, applying insert and delete many times will cause a larger and larger memory footprint, since remove can not remove the element from the underlying data structure, but can only build yet another closure which skips over the removed element. More pragmatically, it would be as if insert and remove simply appended to a log, and search scanned the log to see if the most recent action on an element was an insertion or a removal. This will not have a great performance.

like image 181
chi Avatar answered Nov 13 '22 08:11

chi


When you pattern-match on a Context, you get in the db field a value of a type that you can never know precisely; all you're allowed to know about it is that it's a Database instance, and thus you can use that class' methods with it. But that means that, from the point of view of the Context type, the existential d type affords it no more capabilities than this type does:

-- The "record of methods" pattern
data Database =
  Database { search :: String -> IO [User]
           , fetch  :: Int -> IO (Maybe User)
           }

liveDb :: Database
liveDb = Database search fetch
  where search d q = return $ filter ((q==) . intersect q . show) listUsers
        fetch  d i = return $ if i<3  then Just $ listUsers!!i else Nothing

testDb :: Database
testDb = Database search fetch
  where search _ _ = return [Robot]
        fetch  _ _ = return (Just Robot)

data Context =
  Context { db     :: Database
          , config :: Config
          }

That's the core argument against using existential types in the manner that you've done—there is a completely equivalent alternative that doesn't require existential types.

like image 39
Luis Casillas Avatar answered Nov 13 '22 07:11

Luis Casillas