5
\$\begingroup\$

I've just finished reading Learn You a Haskell and I've started to experiment on my own. I just wanted to create a very simple game system where player A can attack player B.

Now I have 2 questions:

  1. My autoAttack function was very daunting to write. How could I improve it?

  2. How would I format my autoAttack function correctly? I don't find it very pleasing to read.

main = print $ createActor `autoAttack` createActor
type Vector2= (Float,Float)
type Vector3 = (Float,Float,Float)
data StatsSystem = StatsSystem {physicalDamage :: Float,
                                spellPower     :: Float,
                                health         :: Float,
                                mana           :: Float,
                                attackRange    :: Float,
                                castRange      :: Float,
                                attackSpeed    :: Float
                               } deriving(Show)

data MovementSystem = MovementSystem {direction :: Vector2,
                                      speed     :: Float,
                                      position  :: Vector3
                                     } deriving(Show)
data Actor = Actor {stats :: StatsSystem,
                    ms    :: MovementSystem
                   } deriving(Show)
createActor :: Actor
createActor = Actor {stats = StatsSystem {physicalDamage = 1.0,
                                          spellPower = 1.0,
                                          health = 10.0,
                                          mana = 1.0,
                                          attackRange = 1.0,
                                          castRange = 1.0,
                                          attackSpeed = 1.0
                                         },
                     ms = MovementSystem{direction = (1.0,1.0),
                                         speed = 50.0,
                                         position = (0,0,0)
                                        }
                    }
autoAttack :: Actor -> Actor -> Actor
autoAttack Actor {stats = StatsSystem {physicalDamage = p,
                                       spellPower = _,
                                       health = _,
                                       mana = _,
                                       attackRange = _,
                                       castRange = _,
                                       attackSpeed = _
                                      },
                   ms = m
                 } 
           Actor {stats = s2,
                   ms = m2
                 } 
           = Actor {stats = StatsSystem {physicalDamage = physicalDamage s2,
                                         spellPower = spellPower s2,
                                         health = health s2 - p,
                                         mana = mana s2,
                                         attackRange = attackRange s2,
                                         castRange = castRange s2,
                                         attackSpeed = attackSpeed s2
                                        },
                    ms = m2
                   }
\$\endgroup\$
0

3 Answers 3

11
\$\begingroup\$

First, when you are not using fields in a record, you don't have to use _ to mark that they should be ignored; you only need to include the fields in the pattern that you are actually using.

Second, it is possible to update a couple of fields in a record without respecifying the whole thing from scratch. If x is the name of the old record and you want to set the field a to 42 and b to 24, then the expression x { a = 42, b = 24 } is equal to the record x with a set to 42 and b set to 24.

Putting these ideas together, autoAttack could be rewritten like so:

autoAttack :: Actor -> Actor -> Actor
autoAttack actor@(Actor {stats = stats@(StatsSystem {physicalDamage = p, health = h}})) =
    actor {stats = stats { health = h - p } }

Where we have used the syntax actor@(Actor ..) to bind the Actor to the name actor, which is a useful pattern for when you want to both pattern match on an argument and also have access to its value.


Just to be clear, it is worth mentioning that, because data is immutable, changing a couple of fields involves making a full copy of the data structure. (Thanks to Daniel Wagner for bringing this up).

\$\endgroup\$
2
  • 1
    \$\begingroup\$ To be precise: it is in fact not possible (in GHC) to update a couple of fields in a record without rebuilding the whole thing from scratch; however, there is a bit of syntax sugar (which you described well) that makes describing how to rebuild it more palatable than listing the new value of every single field. \$\endgroup\$ Commented Nov 9, 2013 at 4:11
  • 1
    \$\begingroup\$ You are right, I can see how my wording might be confusing. I changed "rebuilding" to "respecifying" and added an end note. \$\endgroup\$ Commented Nov 9, 2013 at 4:49
5
\$\begingroup\$

Two things that can help clean a lot of stuff up are the lens library and the RecordWildCards language extension.

RecordWildCards allows you to set record fields in a very declarative fashion. See the changes to createActor to get an idea of how that can help.

The various functions in lens help a lot when manipulating nested records. autoAttack becomes much shorter and easier to read (once you've gotten used to the various lens combinators).

{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE RecordWildCards #-}

import Control.Lens

data StatsSystem = StatsSystem {
    _physicalDamage :: Float
  , _spellPower     :: Float
  , _health         :: Float
  , _mana           :: Float
  , _attackRange    :: Float
  , _castRange      :: Float
  , _attackSpeed    :: Float
  } deriving Show

data MovementSystem = MovementSystem {
    _direction :: (Float, Float)
  , _speed     :: Float
  , _position  :: (Float, Float, Float)
  } deriving Show

data Actor = Actor {
    _stats :: StatsSystem
  , _ms    :: MovementSystem
  } deriving Show 

makeLenses ''Actor
makeLenses ''StatsSystem
makeLenses ''MovementSystem

createActor :: Actor
createActor = Actor StatsSystem {..} MovementSystem {..} where
  _physicalDamage = 1.0
  _spellPower     = 1.0
  _health         = 10.0
  _mana           = 1.0
  _attackRange    = 1.0
  _castRange      = 1.0
  _attackSpeed    = 1.0
  _direction      = (1.0, 1.0)
  _speed          = 50.0
  _position       = (0.0, 0.0, 0.0)

autoAttack :: Actor -> Actor -> Actor
autoAttack a0 a1 = a1&stats.health -~ a0^.stats.physicalDamage
\$\endgroup\$
1
\$\begingroup\$

Maybe I'm being a bit primitive here, but what's wrong with a couple of helper functions?

punchStrength a = physicalDamage $ stats a
-- or...
-- punchStrength = physicalDamage.stats


hurtActor a amt = a { stats = (stats a) { health = old_health - amt } }
    where old_health = health $ stats a

autoAttack a0 a1 = hurtActor a1 (punchStrength a0)

I'll grant you that the record syntax in hurtActor is a bit ugly, but the lens stuff isn't a huge improvement what with its 9 million line-noise operators.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ They grow on you. ;) \$\endgroup\$
    – jtobin
    Commented Nov 9, 2013 at 20:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.