问题
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.
https://www.schoolofhaskell.com/school/to-infinity-and-beyond/pick-of-the-week/basic-lensing
http://fluffynukeit.com/how-functional-programming-lenses-work/
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 Spellbook
s 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