抽象镜头类型以提供更好的属性读写控制

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)

这样的代码允许我将函数 hitEachOtherpowerUp 与作为 HasHealthHasPower.

实例的任何游戏实体一起使用

这里的问题是 hitEachOther 函数的签名,在当前形式下,它允许编写逻辑来更新来自函数参数的两个实体的 healthpower 属性,虽然我想确保此函数只能更新 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)

问题:

  1. 为什么会报错?我猜这是因为 Len'sGetter 是类型同义词,而不是不同的类型。
  2. 我如何更新我的代码以实现我的目标 - 正确控制我的函数在编译时可以 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 作为用 datanewtype 定义的类型)。请记住,每次使用此 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 参数变体。