Reputation: 125
I'm trying to wrap my head around dependency injection in Scala using monad readers. I started learning Scala recently, so the code I give here, does not compile, but I hope my problem becomes clear. To start, lets assume our application allows a user to changes it password. First, I create a simple case class User and add a changePassword method on the companion object:
case class User (id:Int, username:String, password:String)
object User {
def changePassword (oldPassword:String, newPassword:String, user:User) = {
if (!user.password.equals(oldPassword)) {
-\/("Old password incorrect")
} else {
\/-(user.copy(password = newPassword))
}
}
}
Note that the changePassword method is still a bit to specific in its return type. In Haskell I would write:
data User = User {
id :: Int
, username :: String
, password :: String
} deriving (Show)
changePassword :: (MonadError String m) => String -> String -> User -> m User
changePassword old new user =
if password user == old
then return $ user { password = new }
else throwError "Old password incorrect"
This would allow the changePassword function to be used in any monad transformer stack which contains the Error monad.
Now, to create the application we need two more additional components. One component is a repository which knows how to retrieve and store User objects. Multiple implementations may exists. For example we may have a database repository in production and a in memory repository for testing purposes.
trait UserRepository {
def getById(id:Int):M[User]
def save (user:User):M[Unit]
}
object DatabaseUserRepository extends UserRepository {
def getById(id:Int):MonadReader[Connection,User]
def save (user:User):MonadReader[Connection,Unit]
}
object InMemoryUserRepository extends UserRepository {
def getById(id:Int):MonadState[UserMap,User]
def save (user:User):MonadState[UserMap,Unit]
}
Both implementations are monadic, but the monadic behavior they need may differ. I.e. the database repository depends on a connection which its may access using the reader monad while the in memory repository depends on the state monad.
The other component is a service component which acts as entry point to our logic from the UI.
object UserService {
def doChangePassword (id:Int, oldPassword:String, newPassword:String):MonadReader[UserRepository, Unit]
}
This component uses the user repository to retrieve the user by the given id and then calls the changePassword function and saves the updated user object back using the repository.
I hope this illustrates what I try to achieve. However, I'm still a bit puzzled how to connect the different parts together...
Upvotes: 3
Views: 403
Reputation: 125
To answer my own question, at least partially. I searched google for this topic and found out about the concept of a free monad:
http://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html
After reading this, I came up with:
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE FlexibleContexts #-}
module Main where
import Control.Monad.Free
import Control.Monad.Error
import Control.Monad.Identity
import Control.Monad.State hiding (get)
import qualified Control.Monad.State as MS
import Data.IntMap
import Prelude hiding (lookup)
data User = User {
ident :: Int
, username :: String
, password :: String
} deriving (Show, Eq, Ord)
changePassword' :: (MonadError String m) => String -> String -> User -> m User
changePassword' old new user =
if password user == old
then return $ user { password = new }
else throwError "Old password incorrect"
type UserMap = IntMap User
data Interaction next = Save User next
| Get Int (User -> next)
| ChangePassword String String User (User -> next)
instance Functor Interaction where
fmap f (Save user next) = Save user (f next)
fmap f (Get id g) = Get id (f . g)
fmap f (ChangePassword old new user g) = ChangePassword old new user (f . g)
type Program = Free Interaction
save :: User -> Program ()
save user = liftF (Save user ())
get :: Int -> Program User
get ident = liftF (Get ident id)
changePassword :: String -> String -> User -> Program User
changePassword old new user = liftF (ChangePassword old new user id)
doChangePassword :: String -> String -> Int -> Program ()
doChangePassword old new ident = get ident
>>= changePassword old new
>>= save
newtype ST a = ST { run :: StateT UserMap (ErrorT String Identity) a } deriving (Monad, MonadState UserMap, MonadError String)
runST :: ST a -> UserMap -> UserMap
runST (ST x) s = case runIdentity (runErrorT (execStateT x s)) of
Left message -> error message
Right state -> state
interpreter :: Program r -> ST r
interpreter (Pure r) = return r
interpreter (Free (Save user next)) = do
modify (\map -> insert (ident user) user map)
interpreter next
interpreter (Free (Get id g)) = do
userMap <- MS.get
case lookup id userMap of
Nothing -> throwError "Unknown identifier"
Just user -> interpreter (g user)
interpreter (Free (ChangePassword old new user g)) = do
user' <- changePassword' old new user
interpreter (g user')
main = (putStrLn . show) $ runST (interpreter p) (fromList [(1, User 1 "username" "secret")])
where
p = doChangePassword "secret" "new" 1
Here we define a small language consisting of three operations: Get, Save and ChangePassword. Then we define our function in terms of these 3 operations:
doChangePassword :: String -> String -> Int -> Program ()
doChangePassword old new ident = get ident
>>= changePassword old new
>>= save
The result of this function is simply a structure describing a small program which we need to execute. For this, we write a small interpreter. Changing from a database repository to an in memory repository is achieved by providing a different interpreter.
Composing multiple languages is possible by defining coproducts as described in data types a la carte (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.101.4131&rep=rep1&type=pdf). But until now, I didn't have time yet to try this out.
Upvotes: 1