Haskell Typeclasses & Dependency Injection
By Steven Leiva
There are a multitude of "lenses" with which to view typeclasses in Haskell. We can view typeclasses as an interface; or, we can view typeclasses as a name for a set of related types; or, we can view typeclasses as a form of dependency injection.
In this post, I'd like to talk about viewing a typeclass as dependency injection (on steroids). This is not a new idea, and credit goes to Matt Parsons's, who first explained this to me in a tractable way in his post, Invert Your Mocks!
What is Dependency Injection
For our purposes, we'll define dependency injection as passing behavior to a program, as opposed to hard-coding the behavior into the program. For example:
// Decrease the cost once, and then increase it twice
function computeCost(costIncrease, costDecrease, startingCost) {
const decreasedCost = costDecrease(startingCost)
const increasedCost = costIncrease(decreasedCost)
return costIncrease(increasedCost)
}In the snippet above, we have passed into the computeCost function how to
increase a cost and decrease the cost. The benefit of doing this is that we can
change the behavior of computeCost by passing in different arguments. In one
scenario, maybe increasing our cost means adding 5. In another scenario,
increasing our cost means adding 10. Because this behavior can be passed into
our computeCost function, we can change the behavior as needed.
Now we know what dependency injection is, as well as why it is useful.
"Manual" Dependency Injection in Haskell
Let's translate the above function from JavaScript to Haskell:
computeCost :: (a -> a) -> (a -> a) -> a
computeCost costIncrease costDecrease startingCost =
let decreasedCost = costDecrease startingCost
increasedCost = costIncrease decreasedCost
in costIncrease increasedCostDependency Injection via Typeclasses
Let's now use typeclasses to perform the dependency injection for us. We'll write out the code first, and then explain what is going on:
class TrackCost a where
costDecrease :: a -> a
costIncrease :: a -> a
computeCost :: TrackCost a => a -> a
computeCost startingCost =
let decreasedCost = costDecrease startingCost
increasedCost = costIncrease decreasedCost
in costIncrease increasedCost
newtype HighCost = HighCost Int deriving Show
newtype LowCost = LowCost Int deriving Show
instance TrackCost HighCost where
costDecrease (HighCost currentCost) = HighCost (currentCost + 10)
costIncrease (HighCost currentCost) = HighCost (currentCost - 5)
instance TrackCost LowCost where
costDecrease (LowCost currentCost) = LowCost (currentCost + 5)
costIncrease (LowCost currentCost) = LowCost (currentCost - 2)
main :: IO ()
main = do
print (computeCost (HighCost 10)) # Will print "HighCost 25"
print (computeCost (LowCost 10)) # Will print "LowCost 18"Let's first focus on the behavior of computeCost. Notice that the result of
main proves that we have injected behavior into commputeCost. How? Because
even though we have passed in essentially the same argument - barring the
newtype wrapper, in both cases we are passing in 10 - computeCost resulted
in a different number - specifically, 25 and 18.
What happened is that instead of passing in the behavior explicitly via
arguments to computeCost, we are passing in the behavior implicitly via
typeclasses. The function that costDecrease and costIncrease will resolve to
depend on what the type variable a is specialized to. In the case of
HighCost, costDecrease will resolve to a fuction that subtracts 5, and
costIncrease will resolve to a function that adds 10. You can likely figure
out what costDecrease and costIncrease will resolve to when the type
variable a is specialized to LowCost.
Side Notes
I wrote computeCost in such a way as to keep a symmetry between the JavaScript
function. In the wild, you are most likely to see the Haskell version of
computeCost written with function composition and in point free style:
computeCost :: (a -> a) -> (a -> a) -> a
computeCost costIncrease costDecrease = costIncrease . costIncrease . costDecrease
computeCost' :: TrackCost a => a -> a
computeCost' = costIncrease . costIncrease . costDecrease