简体   繁体   中英

How do I correctly format a function using Control.Lens?

I'm currently learning the lens library by writing some simple functions using the library. Unfortunately, I'm rather confused by the compiler errors generated, and so I'm struggling to determine why, in the function dmg below, the first two functions compile correctly, but the last fails.

import Control.Lens

type Health = Int
type Damage = Int 

data Card = Card {
    _health :: Int,
    _damage :: Int
} deriving (Show,Eq)

health :: Lens' Card Health
health = lens _health (\card h -> card { _health = h })
damage :: Lens' Card Damage
damage = lens _damage (\card d -> card { _damage = d })

cardDead :: Card -> Bool
cardDead c = c^.health <= 0

duel :: (Card,Card) -> (Card,Card)
duel (c,c2) = ((dmg c c2),(dmg c2 c))

The real meat of the matter.

dmg :: Card -> Card -> Card
dmg myCard otherCard = over health ((-) (otherCard^.damage)) myCard --compiles
dmg myCard otherCard = myCard & health %~ ((-) (otherCard^.damage)) --compiles
dmg myCard otherCard = health %~ ((-) (otherCard^.damage)) myCard    --compile error

My question comes in three parts.

  1. Why does the third dmg function fail to compile?

  2. How could I write dmg to still use the (%~) operator, not use (&) , and still compile?

  3. What is the most beautiful, idiomatically lens way of writing dmg ?

--

For reference, here is one way you could would write dmg without lens

dmg myCard otherCard = 
    let 
        damageTaken = _damage otherCard
        oldHealth = _health myCard
        newHealth = oldHealth - damageTaken
    in myCard {_health = newHealth}

edit: For reference, here is the error message I was having trouble understanding with the (incorrectly written) line 3.

*Main GHC.Arr Control.Applicative Control.Lens> :l Doom.hs
[1 of 1] Compiling Main             ( Doom.hs, interpreted )

Doom.hs:26:24:
    Couldn't match expected type `Card' with actual type `Card -> Card'
    In the expression: health %~ ((-) (otherCard ^. damage)) myCard
    In an equation for `dmg':
        dmg myCard otherCard = health %~ ((-) (otherCard ^. damage)) myCard

Doom.hs:26:51:
    Couldn't match type `Health -> Health' with `Int'
    Expected type: Getting (Health -> Health) Card (Health -> Health)
      Actual type: (Damage -> Const (Health -> Health) Damage)
                   -> Card -> Const (Health -> Health) Card
    In the second argument of `(^.)', namely `damage'
    In the first argument of `(-)', namely `(otherCard ^. damage)'

Doom.hs:26:60:
    Couldn't match expected type `Health -> Health'
                with actual type `Card'
    In the second argument of `(-)', namely `myCard'
    In the second argument of `(%~)', namely
      `((-) (otherCard ^. damage)) myCard'
Failed, modules loaded: none.
Prelude GHC.Arr Control.Applicative Control.Lens>
  1. Because parsing rules. Your code is of the form

     abc = def %~ ghi jkl 

    Function application binds tighter than any infix operator, so this is parsed as

     abc = def %~ (ghi jkl) 

    ie what other languages would write def %~ ghi(jkl) .

  2. You want (def %~ ghi) jkl instead. This is commonly accomplished with $ in Haskell, ie

     dmg myCard otherCard = health %~ ((-) (otherCard^.damage)) $ myCard 
  3. First I'd eliminate unnecessary parentheses. Operator sections are usually nicer than infix-applied-to expressions, ie

     dmg myCard otherCard = health %~ ((otherCard^.damage) -) $ myCard 

    ...where the inner parens can be omitted due to

     Prelude> :info Control.Lens.^. ... infixl 8 Control.Lens.Getter.^. Prelude> :i - ... infixl 6 - 

    ie ^. binds more tightly than - anyway, giving

     dmg myCard otherCard = health %~ (otherCard^.damage -) $ myCard 

    Next I'd try η-reduction. This would be easy if the arguments are swapped, which is probably the more Haskell-idiomatic argument order:

     dmg otherCard myCard = health %~ (otherCard^.damage -) $ myCard dmg otherCard = health %~ (otherCard^.damage -) 

    And that's probably the most elegant solution.

That is, assuming your code is actually correct. I don't know what dmg is supposed to do, but perhaps more common is the situation where you want to subtract the other card's damage from yours. Ie basically not (otherCard^.damage -) but (- otherCard^.damage) , except that's parsed as an unary minus and would therefore need to be written subtract (otherCard^.damage) . Lens has dedicated operators for adding and subtracting, giving you

dmg otherCard = health -~ otherCard^.damage

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM