Piszę mały program podobny do gry za pomocą biblioteki lens i zawiera następujący kod:

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)

Taki kod pozwala mi używać funkcji hitEachOther i powerUp z dowolną jednostką gry, która jest instancją HasHealth i HasPower.

Problem tutaj jest podpis funkcji hitEachOther, w bieżącym formularzu pozwala na zapisanie logiki, która może zaktualizować health, a także power Właściwości dwóch podmiotów pochodzących z argumentów funkcyjnych, podczas gdy chcę Upewnij się, że ta funkcja może tylko zaktualizować health i mieć power jako własność tylko do odczytu.

Oznacza, że mogę napisać taki kod (uwaga dodanie 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

Chociaż chcę go zabronić w czasie kompilacji.

Jednym ze sposobów naprawienia jest zmiana HasPower typeClass do

class HasPower a where
  power :: Getter a Int

I rzeczywiście rozwiąże problem dla funkcji hitEachOther, ale uniemożliwią funkcję napisania powerUp.

Miałem małe doświadczenie przy użyciu transformatorów monady i klas, takich jak MonadState s, więc myślałem, aby spróbować uogólnić mój kod w ten sam sposób przy użyciu wielokaramskich liter:

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

Daje więc wymagane ograniczenia w czasie kompilacji, a także będzie bardzo jasne z podpisu funkcyjnego, które hitEachOther może modyfikować health, ale może tylko odczytywać tylko z power, a samo dla power i samego dla power i samego X3}} - Signature mówi, że może zaktualizować power.

Ale taki kod daje mi błąd:

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)

Pytania:

  1. Dlaczego daje błąd? Mogę się domyślić, że Len's i Getter są synonimami, a nie oddzielnymi typami.
  2. Jak mogę zaktualizować mój kod, aby osiągnąć mój cel - mieć odpowiednią kontrolę nad tym, co moja funkcja może czytać / napisać w czasie kompilacji?

-

Pełna kompilowana minimalna próbka oryginalnego kodu:

{-# 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 lipiec 2020, 14:16

1 odpowiedź

Najlepsza odpowiedź

Lens' i Getter są synonimami, więc muszą być zawsze w pełni zastosowane, podczas gdy {X2}} wymaga częściowej aplikacji.

Zamiast parametryzacji HasPower, pamiętaj, że możesz więcej mieć dwie klasy:

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

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

Jeśli naprawdę chcesz uniknąć duplikacji, jednym rozwiązaniem jest owinięcie synonim typu A NewTytYpe , które można zastosować (innymi słowy, typ synonimy nie są jako pierwszej klasy jako typy zdefiniowane z { {X0}} i newtype). Pamiętaj, że za każdym razem, gdy używasz tej klasy, będziesz musiał go rozwinąć, dzięki czemu używasz, czy używasz wersji "Tylko do odczytu" lub "Odczyt":

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

Należy pamiętać, że niektóre wariant tych NewTtypes istnieją w { {X0}}, choć tylko 4-parametr wariant do soczewek.

3
Li-yao Xia 17 lipiec 2020, 14:17