抽象镜头类型以提供更好的属性读写控制
Abstract over lens type to provide better read-write control of properties
我正在使用 lens
库编写类似游戏的小程序,代码如下:
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)
这样的代码允许我将函数 hitEachOther
和 powerUp
与作为 HasHealth
和 HasPower
.
实例的任何游戏实体一起使用
这里的问题是 hitEachOther
函数的签名,在当前形式下,它允许编写逻辑来更新来自函数参数的两个实体的 health
和 power
属性,虽然我想确保此函数只能更新 health
,并将 power
设置为只读 属性.
表示我可以写这样的代码(注意添加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
虽然我想在编译时禁止它。
修复它的一种方法是将 HasPower
类型类更改为
class HasPower a where
power :: Getter a Int
确实可以解决hitEachOther
函数的问题,但是会导致无法编写powerUp
函数。
我在使用 monad 转换器和 类 方面经验不足 MonadState s
,所以我想尝试使用多参数类型 类 以相同的方式概括我的代码:
{-# 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)
所以它会在编译时给出所需的限制,并且从函数签名中也很清楚hitEachOther
可以修改health
,但只能从power
读取,和 powerUp
一样 - 签名说它可以更新 power
.
但是这样的代码给我错误:
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)
问题:
- 为什么会报错?我猜这是因为
Len's
和 Getter
是类型同义词,而不是不同的类型。
- 我如何更新我的代码以实现我的目标 - 正确控制我的函数在编译时可以 read/write 做什么?
--
原始代码的完整可编译最小示例:
{-# 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)
Lens'
和 Getter
是类型同义词,因此必须始终完全应用它们,而 HasPower Lens' a
需要部分应用。
而不是参数化 HasPower
,请注意,您可以更简单地拥有两个 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
如果你真的想避免重复,一个解决方案是将类型同义词包装在一个 newtype 中,这可以应用(换句话说,类型同义词不像first-class 作为用 data
和 newtype
定义的类型)。请记住,每次使用此 class 时都必须将其解包,明确说明您使用的是“只读”还是“读写”版本:
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_ (...)
请注意,Control.Lens.Reified
中存在这些新类型的某些变体,但只有镜头的 4 参数变体。
我正在使用 lens
库编写类似游戏的小程序,代码如下:
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)
这样的代码允许我将函数 hitEachOther
和 powerUp
与作为 HasHealth
和 HasPower
.
这里的问题是 hitEachOther
函数的签名,在当前形式下,它允许编写逻辑来更新来自函数参数的两个实体的 health
和 power
属性,虽然我想确保此函数只能更新 health
,并将 power
设置为只读 属性.
表示我可以写这样的代码(注意添加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
虽然我想在编译时禁止它。
修复它的一种方法是将 HasPower
类型类更改为
class HasPower a where
power :: Getter a Int
确实可以解决hitEachOther
函数的问题,但是会导致无法编写powerUp
函数。
我在使用 monad 转换器和 类 方面经验不足 MonadState s
,所以我想尝试使用多参数类型 类 以相同的方式概括我的代码:
{-# 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)
所以它会在编译时给出所需的限制,并且从函数签名中也很清楚hitEachOther
可以修改health
,但只能从power
读取,和 powerUp
一样 - 签名说它可以更新 power
.
但是这样的代码给我错误:
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)
问题:
- 为什么会报错?我猜这是因为
Len's
和Getter
是类型同义词,而不是不同的类型。 - 我如何更新我的代码以实现我的目标 - 正确控制我的函数在编译时可以 read/write 做什么?
--
原始代码的完整可编译最小示例:
{-# 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)
Lens'
和 Getter
是类型同义词,因此必须始终完全应用它们,而 HasPower Lens' a
需要部分应用。
而不是参数化 HasPower
,请注意,您可以更简单地拥有两个 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
如果你真的想避免重复,一个解决方案是将类型同义词包装在一个 newtype 中,这可以应用(换句话说,类型同义词不像first-class 作为用 data
和 newtype
定义的类型)。请记住,每次使用此 class 时都必须将其解包,明确说明您使用的是“只读”还是“读写”版本:
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_ (...)
请注意,Control.Lens.Reified
中存在这些新类型的某些变体,但只有镜头的 4 参数变体。