Imagine simple game:

type Combatant = {
    hp : int
    attack : int

type CombatantGroup = Combatant list
type CombatantGroups = CombatantGroup list

type Battle = {
    combatantGroups : CombatantGroups

there is some battle, in which some amount groups participate. In each group there is several combatants. Now I want to implement a function for one combatant to attack another. Due to immutability, I have to replace whole attacked target, and whole group and whole battle. If the battle was just between two participants, I could write code like this:

if target = battle.combatant1 then
    { battle with combatant1 = { battle.combatant1 with hp = battle.combatant1.hp - attacker.attack }}
    { battle with combatant2 = { battle.combatant2 with hp = battle.combatant2.hp - attacker.attack }}

which is also terrible code, I would appreciate any advice on it. But with nested collections it gets even more complicated, and event doesn't look functional to me anymore:

let containsTarget = List.contains target        
let transformCombatant combatant = if combatant = target then { combatant with hp = combatant.hp - attacker.attack } else combatant        
let transformGroup = List.map transformCombatant                
let checkGroup group =
    if group |> containsTarget then
        group |> transformGroup
let transformGroups = List.map checkGroup

{ battle with combatantGroups = transformGroups battle.combatantGroups }

Can you please give me advise on how can I use features of F# and functional programming, to make given code more pretty and concise?

  • I think you need an id field to be sure that you're updating the correct combatant. I assume it's possible to have multiple combatants with the same hp and attack.
  • And in that case it makes sense to store a group as a Map of id to combatant. This makes it really simple to update a value by id and there's no need to check for existence first. See the tryUpdateCombatantById function below.
  • You can add small helper functions to update specific record fields with a function that is passed in.

Here's the code with all of those changes:

type Combatant = {
    id : int
    hp : int
    attack : int

type Battle = { combatantGroups : Map<int, Combatant> list }

let updateHp f combatant = { combatant with hp = f combatant.hp }
let removeHp attack = updateHp (fun hp -> hp - attack)

let tryUpdateCombatantById combatantId f group =
    |> Map.tryFind combatantId
    |> Option.map (fun c -> group |> Map.add c.id (f c))
    |> Option.defaultValue group

let applyAttack targetId attacker battle =
    { battle with
        combatantGroups =
            |> List.map (tryUpdateCombatantById targetId (removeHp attacker.attack)) }
|improve this answer|||||
  • \$\begingroup\$ Option.map in combination with Option.defaultValue seems like a very good pattern that I was not aware of, but I think your code actually adds new combat participant, instead of apdating an existing one, isn't it? \$\endgroup\$ – Dmitry Pavlushin Apr 8 '19 at 20:23
  • 1
    \$\begingroup\$ Map.add adds a value or replaces an existing one and in this case we're only adding values when the key already exists. An F# map is like a dictionary or hash table but immutable. \$\endgroup\$ – TheQuickBrownFox Apr 8 '19 at 22:04
  • \$\begingroup\$ Oh, right, that's a map, and not a list, thx for clarification \$\endgroup\$ – Dmitry Pavlushin Apr 9 '19 at 6:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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