oncise way of “updating” element in nested collections

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 }}
else
    { 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
    else
        group        
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?

Can you providing verifiable code? From the snippet it’s not obvious where attacker from the transformCombatant function and target for containsTarget are defined.

Yet I can write few comments:

I’d put { combatant with hp = combatant.hp - attacker.attack } into separate function or even operator:

let update combatant attacker = 
    { combatant with hp = combatant.hp - attacker.attack }

(with meaningful naming, of course - update here is just for example)

Also not sure it is really good using of List.contains. It can even give worst performance in some cases.

Edit:

If you’re looking for some functional concepts that fits for updating of nested type then there are lenses and prisms:

Lenses

Prisms

Libraries:

Aether - optics for F#
FSharpPlus - A complete and extensible base library for F#

I didn’t use them but who knows maybe it help you.