Reputation:
I'm writing small game-like program using lens
library and have the following code:
class HasHealth a where
health :: Lens' a Int
class HasPower a where
power :: Lens' a Int
hitEachOther :: (HasHealth a, HasPower a, HasHealth b, HasPower b) => a -> b -> (a, b)
hitEachOther first second = (firstAfterHit, secondAfterHit)
where
secondAfterHit = first `hit` second
firstAfterHit = second `hit` first
hit damageDealer unitUnderHit = health `over` subtract (view power damageDealer) $ unitUnderHit
powerUp :: HasPower a => a -> a
powerUp = power `over` (+10)
Such code allows me to use functions hitEachOther
and powerUp
with any game entity which is instance of HasHealth
and HasPower
.
The problem here is the signature of hitEachOther
function, in current form it allows to write logic which can update health
and also power
properties of two entities coming from function arguments, while I want to make sure this function can update health
only, and have power
as read-only property.
Means I can write such code (note addition of power `over` (+1)
):
hitEachOtherBad :: (HasHealth a, HasPower a, HasHealth b, HasPower b) => a -> b -> (a, b)
hitEachOtherBad first second = (firstAfterHit, power `over` (+1) $ secondAfterHit)
where
secondAfterHit = first `hit` second
firstAfterHit = second `hit` first
hit damageDealer unitUnderHit = health `over` subtract (view power damageDealer) $ unitUnderHit
though I want to prohibit it at compile-time.
One way to fix it is to change HasPower
typeclass to
class HasPower a where
power :: Getter a Int
and it will indeed solve the issue for hitEachOther
function, but will make it impossible to write powerUp
function.
I had small experience of using monad transformers and classes like MonadState s
, so was thinking to try to generalize my code in the same way using multiparam typeclasses:
{-# LANGUAGE MultiParamTypeClasses #-}
class HasHealth l a where
health :: l a Int
class HasPower l a where
power :: l a Int
hitEachOther :: (HasHealth Lens' a, HasPower Getter a, HasHealth Lens' b, HasPower Getter b) => a -> b -> (a, b)
hitEachOther first second = (firstAfterHit, secondAfterHit)
where
secondAfterHit = first `hit` second
firstAfterHit = second `hit` first
hit damageDealer unitUnderHit = health `over` subtract (view power damageDealer) $ unitUnderHit
powerUp :: HasPower Lens' a => a -> a
powerUp = power `over` (+1)
So it will give required restrictions at compile-time and it also will be very clear from function signature that hitEachOther
can modify health
, but can only read from power
, and same for powerUp
- signature says it can update power
.
But such code gives me error:
error:
* The type synonym Lens' should have 2 arguments, but has been given none
* In the type signature:
hitEachOther :: (HasHealth Lens' a, HasPower Getter a, HasHealth Lens' b, HasPower Getter b) => a -> b -> (a, b)
Questions:
Len's
and Getter
are type synonyms, not separate types.--
Full compilable minimal sample of original code:
{-# LANGUAGE TemplateHaskell #-}
module Main where
import Control.Lens
data Hero = Hero {_heroName :: String, _heroHealthPoints :: Int, _heroMoney :: Int, _heroPower :: Int} deriving Show
data Dragon = Dragon {_dragonHealthPoints :: Int, _dragonPower :: Int} deriving Show
makeLenses ''Hero
makeLenses ''Dragon
myHero :: Hero
myHero = Hero "Bob" 100 0 15
myDragon :: Dragon
myDragon = Dragon 300 40
main :: IO ()
main = do
let (heroAfterFight, dragonAfterFight) = hitEachOther myHero myDragon
let heroAfterPowerUp = powerUp heroAfterFight
print heroAfterPowerUp
print dragonAfterFight
class HasHealth a where
health :: Lens' a Int
class HasPower a where
power :: Lens' a Int
instance HasHealth Hero where
health = heroHealthPoints
instance HasHealth Dragon where
health = dragonHealthPoints
instance HasPower Dragon where
power = dragonPower
instance HasPower Hero where
power = heroPower
hitEachOther :: (HasHealth a, HasPower a, HasHealth b, HasPower b) => a -> b -> (a, b)
hitEachOther first second = (firstAfterHit, secondAfterHit)
where
secondAfterHit = first `hit` second
firstAfterHit = second `hit` first
hit damageDealer unitUnderHit = health `over` subtract (view power damageDealer) $ unitUnderHit
powerUp :: HasPower a => a -> a
powerUp = power `over` (+10)
Upvotes: 2
Views: 81
Reputation: 33569
Lens'
and Getter
are type synonyms, so they must always be fully applied, whereas HasPower Lens' a
requires a partial application.
Rather than parameterizing HasPower
, note that you can more simply have two classes:
-- "Read-only" access to power
class HasPowerR a where
powerR :: Getter a Int
-- Read-Write access
class HasPower a where
power :: Lens' a Int
If you really want to avoid the duplication, one solution is to wrap the type synonym in a newtype, which can be applied (in other words, type synonyms are not as first-class as types defined with data
and newtype
). Remember that everytime you use this class you will have to unwrap it, making explicit whether you are using the "read-only" or the "read-write" version:
newtype R s a = Getter_ { unR :: Getter s a } -- read-only
newtype RW s a = Lens_ { unRW :: Lens' s a } -- read-write
class HasPower l a where
power :: l a Int
instance HasPower R a where
power = Getter_ (...)
instance HasPower RW a where
power = Lens_ (...)
Note that some variant of those newtypes exist in Control.Lens.Reified
, though only the 4-parameter variant for lenses.
Upvotes: 2