How to move from external function to functional lens

别说谁变了你拦得住时间么 提交于 2019-12-11 01:45:20

问题


On my journey to start getting better with functional programming, I discovered, with the help of a member of the SO family, what lens. I even made some research on it with the links down below to understand more about them.

  1. https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/basic-lensing

  2. http://fluffynukeit.com/how-functional-programming-lenses-work/

  3. https://medium.com/@dtipson/functional-lenses-d1aba9e52254#.27yw4gnwk

With all that knowledge, I thought I could give them a try and see whether or not I could understand their functionnality and the reasons why they're useful in FP. My problem at the moment is moving from the type members that were define to access and modify fields in my equipment record that I've define for a game that I'm prototyping at the moment. I will put snippets of the Equipment records, the members that were there before and the functional lens I'm trying to create but just won't work. After the first pattern matching, it expects the code to have the same return value, when I'd like it to be a general value to be returned, depending on a pattern that I had successfully matched! For the the remain of the code, instead of omitting the code and making it not compile while you're trying to give me a hand, I've thought it be best to put the important snippets here and a public to the relevant code so you can compile it on your local machine ! The public gist can be found here. It's a lot of for my definitions, the relevant code is from line 916.

type Equipment = {
    Helmet      : Hat option
    Armor       : Armor option
    Legs        : Pants option
    Gloves      : Gauntlets option
    Ring        : Ring option
    Weapon      : Weaponry option
    Shield      : Shield option
    Loot        : ConsumableItem option
}

let equipPurchasedProtection newItem (inventory,equipment) =
    match newItem with
    | Helmet ->
        match equipment.Helmet with
        | None ->
            let newEquipment = { equipment with Helmet = Some newItem }
            (inventory,newEquipment)
        | Some oldHelm
            if (playerWantsToAutoEquip newItem) then
                let newEquipment = { equipment with Helmet = Some newItem }
                let newInventory = inventory |> addToInventory oldHelm
                (newInventory,newEquipment)
            else
                let newInventory = inventory |> addToInventory newItem
                (newInventory,equipment)
    | Gloves ->
        match equipment.Hands with
        | None ->
            let newEquipment = { equipment with Hands = Some newItem }
            (inventory,newEquipment)
        | Some oldGloves
            if (playerWantsToAutoEquip newItem) then
                let newEquipment = { equipment with Hands = Some newItem }
                let newInventory = inventory |> addToInventory oldGloves
                (newInventory,newEquipment)
            else
                let newInventory = inventory |> addToInventory newItem
                (newInventory,equipment)
    | Boots ->
        match equipment.Feet with
        | None ->
            let newEquipment = { equipment with Boot = Some newItem }
            (inventory,newEquipment)
        | Some oldBoots
            if (playerWantsToAutoEquip newItem) then
                let newEquipment = { equipment with Boot = Some newItem }
                let newInventory = inventory |> addToInventory oldBoots
                (newInventory,newEquipment)
            else
                let newInventory = inventory |> addToInventory newItem
                (newInventory,equipment)

let equipPurchasedItem newItem (inventory,equipment) =
    let equipFunction =
        match newItem with
        | Protection(Helmet(_)) -> genericEquipFunction HelmetFun_
        | Protection(Gloves(_)) -> genericEquipFunction GlovesFun_
        | Protection(Legs(_))  -> genericEquipFunction LegsFun_
        | Protection(Armor(_)) -> genericEquipFunction ArmorFun_
        | Protection(Ring(_)) -> genericEquipFunction RingFun_
        | Protection(Shield(_)) -> genericEquipFunction ShieldFun_
        | Weapon _ -> genericEquipFunction WeaponFun_
        | Consumable HealthPotion -> genericEquipFunction LootFun_
        | Consumable HighHealthPotion -> genericEquipFunction LootFun_
        | Consumable MegaHealthPotion -> genericEquipFunction LootFun_
        | Consumable Elixir -> genericEquipFunction LootFun_
        | Consumable HighElixir -> genericEquipFunction LootFun_
        | Consumable MegaElixir -> genericEquipFunction LootFun_
        | Consumable PhoenixFeather -> genericEquipFunction LootFun_
        | Consumable MedicinalHerb -> genericEquipFunction  LootFun_

    let itemForInventory,newEquipment = equipFunction (Some newItem) equipment
    match itemForInventory with
    | None -> (inventory,newEquipment)
    | Some item ->
        let newInventory = inventory |> addToInventory { Item = item; Count = 1 }
        (newInventory,newEquipment)

UPDATE 1 Here's a look at one of the lens function that I'm using to equip purchased items.

let getArmorFun e = e.Armor
let equipArmorFun newArmor e = { e with Armor = newArmor }
let ArmorFun_ = (getArmorFun, equipArmorFun)

回答1:


Having looked at your model more closely, I can confirm my initial impression: you're using a lot more types than you should. Many of those types should be instances; in this case, record instances. Here's a good rule-of-thumb for when you should use a type or an instance. If the two things are interchangeable, they should be two instances of the same type. If they're NOT interchangeable, then (and only then) they should be two different types. Here's an example of what I mean. Here's a section of your code that takes up an entire screen:

type Weaponry =
    | Dagger        of Dagger
    | Sword         of Sword
    | Axe           of Axe
    | Spear         of Spear
    | Staff         of Staff
    | LongBlade     of Blade
    | Spellbook     of Spellbook
with
    member x.Name =
        match x with
        | Dagger d -> d.ToString()
        | Sword  s -> s.ToString()
        | Axe    a -> a.ToString()
        | Spear  s -> s.ToString()
        | Staff  s -> s.ToString()
        | LongBlade lb -> lb.ToString()
        | Spellbook sb -> sb.ToString()
    member x.Price =
        match x with
        | Dagger     w -> w.Price
        | Sword      w -> w.Price
        | Axe        w -> w.Price
        | Spear      w -> w.Price
        | Staff      w -> w.Price
        | LongBlade  w -> w.Price
        | Spellbook  w -> w.Price
    member x.Weight =
        match x with
        | Dagger     w -> w.Weight
        | Sword      w -> w.Weight
        | Axe        w -> w.Weight
        | Spear      w -> w.Weight
        | Staff      w -> w.Weight
        | LongBlade  w -> w.Weight
        | Spellbook  w -> w.Weight

    member x.Stats =
        match x with
        | Dagger     w -> w.WeaponStats :> IStats
        | Sword      w -> w.WeaponStats :> IStats
        | Axe        w -> w.WeaponStats :> IStats
        | Spear      w -> w.WeaponStats :> IStats
        | Staff      w -> w.WeaponStats :> IStats
        | LongBlade  w -> w.WeaponStats :> IStats
        | Spellbook  w -> w.SpellStats  :> IStats

What's different between all these items? The last line, where Spellbooks have SpellbookStats instead of WeaponStats. That's it! As for your other weapon types -- dagger, sword, axe, spear, etc... they're ALL identical in "shape". They all have weapon stats, price, weight, etc.

Here's a redesign of that entire weapon model:

type ItemDetails = { Weight: float<kg>; Price: int<usd> }

type PhysicalWeaponType =
    | Dagger
    | Sword
    | Axe
    | Spear
    | Staff
    | LongBlade

type MagicalWeaponType =
    | Spellbook
    // Could later add wands, amulets, etc.

type WeaponDetails =
    | PhysicalWeapon of PhysicalWeaponType * WeaponStat
    | MagicalWeapon of MagicalWeaponType * SpellbookStats

type Weaponry =
    { Name: string
      ItemDetails: ItemDetails
      WeaponDetails: WeaponDetails }
    with member x.Weight = x.ItemDetails.Weight
         member x.Price  = x.ItemDetails.Price
         member x.Stats  = match x.WeaponDetails with
                           | PhysicalWeapon (_, stats) -> stats :> IStats
                           | MagicalWeapon  (_, stats) -> stats :> IStats

// Now let's create some weapons. In the real game this would be read
// from a JSON file or something, so that the game is easily moddable
// by end users who want to add their own custom weapons.

let rustedDagger = {
    Name = "Rusted dagger"
    ItemDetails = { Weight = 2.10<kg>; Price = 80<usd> }
    WeaponDetails = PhysicalWeapon (Dagger, { Damage = 5.60<dmg>; Defense = 1.20<def>; Intelligence = None; Speed = 1.00<spd>; Critical = 0.02<ctr>; HitLimit = 20<hl>; Rank = RankE })
}

let ironDagger = {
    Name = "Iron dagger"
    ItemDetails = { Weight = 2.80<kg>; Price = 200<usd> }
    WeaponDetails = PhysicalWeapon (Dagger, { Damage = 9.80<dmg>; Defense = 2.30<def>; Intelligence = None; Speed = 1.10<spd>; Critical = 0.04<ctr>; HitLimit = 25<hl>; Rank = RankD })
}

let steelDagger = {
    Name = "Steel dagger"
    ItemDetails = { Weight = 4.25<kg>; Price = 350<usd> }
    WeaponDetails = PhysicalWeapon (Dagger, { Damage = 13.10<dmg>; Defense = 3.00<def>; Intelligence = None; Speed = 1.15<spd>; Critical = 0.05<ctr>; HitLimit = 30<hl>; Rank = RankC })
}

let brokenSword = {
    Name = "Broken sword"
    ItemDetails = { Weight = 7.20<kg>; Price = 90<usd> }
    WeaponDetails = PhysicalWeapon (Sword, { Damage = 5.40<dmg>; Defense = 2.50<def>; Intelligence = None; Speed = 1.20<spd>; Critical = 0.01<ctr>; HitLimit = 10<hl>; Rank = RankE })
}

let rustedSword = {
    Name = "Rusted sword"
    ItemDetails = { Weight = 8.50<kg>; Price = 120<usd> }
    WeaponDetails = PhysicalWeapon (Sword, { Damage = 8.75<dmg>; Defense = 2.90<def>; Intelligence = None; Speed = 1.05<spd>; Critical = 0.03<ctr>; HitLimit = 20<hl>; Rank = RankD })
}

// And so on for iron and steel swords, plus all your axes, spears, staves and long blades.
// They should all be instances, not types. And spellbooks, too:

let rank1SpellbookDetails = { Weight = 0.05<kg>; Price = 150<usd> }
let rank2SpellbookDetails = { Weight = 0.05<kg>; Price = 350<usd> }

let bookOfFireball = {
    Name = "Fireball"
    ItemDetails = rank1SpellbookDetails
    WeaponDetails = MagicalWeapon (Spellbook, { Damage = 8.0<dmg>; AttackRange = 1; Rank = RankE; Uses = 30 ; ManaCost = 12.0<mp> })
}

// Same for Thunder and Frost

let bookOfHellfire = {
    Name = "Hellfire"
    ItemDetails = rank2SpellbookDetails
    WeaponDetails = MagicalWeapon (Spellbook, { Damage = 6.50<dmg>; AttackRange = 2; Rank = RankD; Uses = 25; ManaCost = 20.0<mp> })
}

// And so on for Black Fire and Storm of Blades

let computeCharacterOverallOffensive
    // (rank: WeaponRank)  // Don't need this parameter now
    (weapon: Weaponry)
    (cStats: CharacterStats) =

    let weaponDamage =
        match weapon.WeaponDetails with
        | PhysicalWeapon (_, stats) -> stats.Damage
        | MagicalWeapon  (_, stats) -> stats.Damage

    let weaponRank =
        match weapon.WeaponDetails with
        | PhysicalWeapon (_, stats) -> stats.Rank
        | MagicalWeapon  (_, stats) -> stats.Rank

    // This should really be a method on the Rank type
    let rankMultiplier =
        match weaponRank with
        | RankE -> 1.0100
        | RankD -> 1.0375
        | RankC -> 1.0925
        | RankB -> 1.1250
        | RankA -> 1.1785
        | RankS -> 1.2105

    cStats.Strength * rankMultiplier * weaponDamage

Notice how all the details of the Weaponry type fit on one screen now? And there's WAY less duplication. I kept the distinction between different types of physical weapons (daggers, swords, etc) since it's likely that you'll have characters that specialize in one or two types: a sword specialist can't use an axe, or he takes a 50% strength penalty when he uses an axe, and so on. But I doubt that you're ever going to have a character who can only use iron daggers but can't use steel daggers. Different types of daggers are completely interchangeable in this kind of game -- the player would be VERY surprised if they weren't. So they shouldn't be different types. And the various types of physical weapons are almost interchangeable, so their models should be as similar as possible, too. Put the stats in the part that doesn't differ, and leave the type (Dagger, Sword, Axe) as the only difference between the physical weapons.

This has been a really long answer and I still haven't gotten into your actual question about lenses! But since I winced on looking at the code and thought, "He is making WAY too much work for himself", I had to address this part first.

I think you'd benefit from taking your code over to https://codereview.stackexchange.com/ and asking people there to take a look at it and suggest ways to tighten up your model. Once your model is improved, I think you'll find the lens code to be a lot easier to write as well. And as I said before, DON'T try to write the lens code on your own! Use a library like Aether or F#+ to help you. In your shoes, I'd probably go with Aether simply because it has more documentation than F#+ seems to have; F#+ seems (AFAICT) to be more aimed at people who have already used Haskell lenses and don't need any reminders about how to use them.

UPDATE 1: Have another snippet for how I'd suggest you do armor:

type CharacterProtectionStats = {
    Defense : float<def>
    Resistance : float<res>
    Intelligence : float<intel> option
    MagicResist : float<mgres>
    Speed  : float<spd>
    EquipmentUsage : int<eu>
}
with
    interface IStats with
        member x.showStat() =
            sprintf "Defense : %O - Resistance : %O - Magic resistance : %O - Speed : %O - Equipment usage : %O" x.Defense x.Resistance x.MagicResist x.Speed x.EquipmentUsage

type CharacterProtectionDetails = {
    Name : string
    // No Type field here, because that's staying in the DU
    ItemDetails : ItemDetails
    ArmorStats : CharacterProtectionStats
}


type Hat = Hat of CharacterProtectionDetails
type Armor = Armor of CharacterProtectionDetails
type Pants = Pants of CharacterProtectionDetails
// etc.

type CharacterProtection =
    | Shield        of Shield
    // | Ring          of Ring  // REMOVED. Rings are different; see below.
    | Gloves        of Gauntlets
    | Legs          of Pants
    | Armor         of Armor
    | Helmet        of Hat

let sorcererHat = Hat {
    Name = "Sorcerer Hat"
    ItemDetails = { Weight = 1.0<kg>; Price = 120<usd> }
    ArmorStats = { Defense = 1.20<def>; Resistance = 1.30<res>; Intelligence = Some 3.00<intel>; MagicResist = 1.80<mgres>; Speed = 1.00<spd>; EquipmentUsage = 100<eu> }
}

// Other hats...

let steelArmor = Armor.Armor {
    Name = "Steel Armor"
    ItemDetails = { Weight = 15.0<kg>; Price = 450<usd> }
    ArmorStats = { Defense = 17.40<def>; Resistance = 6.10<res>; Intelligence = None; MagicResist = 2.30<mgres>; Speed = 0.945<spd>; EquipmentUsage = 100<eu> }
}

// "Armor.Armor" is kind of ugly, but otherwise it thinks "Armor" is
// CharacterProtection.Armor. If we renamed the CharacterProtection DU
// item to ChestProtection instead, that could help.

type AccessoryStats = {
    ExtraStrength : float<str> option
    ExtraDamage   : float<dmg> option
    ExtraHealth   : float<hp> option
    ExtraMana     : float<mp> option
}
with
    interface IStats with
        member x.showStat() =
            sprintf ""
    static member Initial =
        { ExtraDamage = None; ExtraStrength = None; ExtraHealth = None; ExtraMana = None }

type Ring = {
    Name : string
    ItemDetails : ItemDetails
    RingStats : AccessoryStats
}

type Amulet = {
    Name : string
    ItemDetails : ItemDetails
    AmuletStats : AccessoryStats
}

type AccessoryItems =
    | Ring   of Ring
    | Amulet of Amulet
    // Could add other categories too

let standardRingDetails = { Weight = 0.75<kg>; Price = 275<usd> }

let strengthRing = {
    Name = "Extra strength ring"
    ItemDetails = standardRingDetails
    RingStats = { RingStats.Initial with ExtraStrength = Some 4.50<str> }
}

let damageRing = {
    Name = "Extra damage ring"
    ItemDetails = standardRingDetails
    RingStats = { RingStats.Initial with ExtraDamage = Some 5.00<dmg> }
}


来源:https://stackoverflow.com/questions/41031331/how-to-move-from-external-function-to-functional-lens

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!