Estoy escribiendo un pequeño programa similar a un juego usando la biblioteca lens y tengo el siguiente código:

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)

Dicho código me permite usar las funciones hitEachOther y powerUp con cualquier entidad del juego que sea una instancia de HasHealth y HasPower.

El problema aquí es la firma de la función hitEachOther, en su forma actual permite escribir lógica que puede actualizar health y también power propiedades de dos entidades provenientes de argumentos de función, mientras que quiero asegúrese de que esta función solo pueda actualizar health y tenga power como propiedad de solo lectura.

Significa que puedo escribir dicho código (tenga en cuenta la adición de 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

Aunque quiero prohibirlo en tiempo de compilación.

Una forma de solucionarlo es cambiar HasPower typeclass a

class HasPower a where
  power :: Getter a Int

Y de hecho resolverá el problema de la función hitEachOther, pero hará imposible escribir la función powerUp.

Tenía poca experiencia en el uso de transformadores de mónadas y clases como MonadState s, por lo que estaba pensando en intentar generalizar mi código de la misma manera usando clases de tipos multiparam:

{-# 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)

Por lo tanto, dará las restricciones requeridas en tiempo de compilación y también estará muy claro a partir de la firma de la función que hitEachOther puede modificar health, pero solo puede leer desde power, y lo mismo para powerUp: la firma dice que se puede actualizar power.

Pero tal código me da un 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)

Preguntas:

  1. ¿Por qué da error? Supongo que se debe a que Len's y Getter son sinónimos de tipo, no tipos separados.
  2. ¿Cómo puedo actualizar mi código para lograr mi objetivo: tener el control adecuado de lo que mi función puede leer / escribir en tiempo de compilación?

--

Muestra mínima compilable completa del código original:

{-# 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)
2
xzt 17 jul. 2020 a las 14:16

1 respuesta

La mejor respuesta

Lens' y Getter son sinónimos de tipo, por lo que siempre deben aplicarse por completo, mientras que HasPower Lens' a requiere una aplicación parcial.

En lugar de parametrizar HasPower, tenga en cuenta que puede tener dos clases más simplemente:

-- "Read-only" access to power
class HasPowerR a where
  powerR :: Getter a Int

-- Read-Write access
class HasPower a where
  power :: Lens' a Int

Si realmente desea evitar la duplicación, una solución es envolver el sinónimo de tipo en un newtype , que se puede aplicar (en otras palabras, los sinónimos de tipo no son tan de primera clase como los tipos definidos con { {X0}} y newtype). Recuerda que cada vez que uses esta clase tendrás que desenvolverla, haciendo explícito si estás usando la versión "solo lectura" o la versión "lectura-escritura":

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_ (...)

Tenga en cuenta que existe alguna variante de esos nuevos tipos en { {X0}}, aunque solo la variante de 4 parámetros para lentes.

3
Li-yao Xia 17 jul. 2020 a las 14:17