如何使用高阶函数来过滤和匹配记录中的选项字段 [英] How to use high order functions to filter and match on options fields in a record

查看:92
本文介绍了如何使用高阶函数来过滤和匹配记录中的选项字段的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

因此,需要一些上下文信息才能理解我的问题.在RPG中,我将设备视为具有可选字段的记录,这意味着尽管它们为None,但尚无任何属性.设备是由游戏的游戏项目构成的,代表了武器或角色保护(头盔等),将在下面的代码段中显示.删除了功能,缩小了域模型,使其更易于阅读.

So a bit of context is required just to understand my issue. In an RPG, I consider the equipment as a record with optional fields, which means, while they're None that nothing was attributed yet. Equipment is constructed by the game items of the game that represents either weaponry or character protection (helmets and etc) which will be seen in the snippet below. The functionalities are removed and the domain model reduced to make it easier to read.

type ConsummableItem =
    | HealthPotion
    | ManaPotion 

type Weaponry = 
    | Sword 
    | Spear 
    | BattleAxe 

type CharaterProtection = 
    | Helmet 
    | Gloves 
    | Boots 

type GameItem = 
    | Consumable of ConsummableItem * int
    | Weapon of Weaponry * int
    | Protection of CharacterProtection * int

 type Inventory = {
     Bag : GameItem array 
 }
 with // code omitted in the add function
     member x.addSingleItem (item: GameItem) = 
         let oItemIndex = x.Bag |> Array.tryFindIndex(fun x -> x = gi)
         match oItemIndex with
         | Some index ->
             let item = x.Bag.[index]
             let bag =  x.Bag
                    bag.[index] <- item
                    { x with Bag = bag }
         | None ->
             let newBag = x.Bag |> Array.append [|gi|]
             { x with Bag = newBag }

type Equipment = { 
     Helmet : Protection option 
     Weapon : Weapon option
     Boot   : Protection option
     Hands  : Protection option 
     Loot1  : Consummable option
     Loot2  : Consummable option
}

我遇到的问题是:玩家从商店购买物品后,他可能想直接在角色设备中转移物品.因此,我正在设计一个将项目放入设备的功能.如果那个领域很忙,那么我将把物品从设备发送回库存,并用商店的物品更新角色的设备.

What I'm having a problem with is the following: once a player buys an item from the store, he may want to transfer the item directly in a character equipment. So, I'm designing a function to put an item in the equipment. If that field was busy, then I would to send the item from the equipment back to the inventory and update the character's equipment with the store's item.

要开始解决我的问题,我认为我可以将这些字段作为GameItem选项的列表,并找出其中一些,然后在该列表中进行迭代,如果我可以找到相同类型的项目作为我要添加到设备中的设备.我有一些问题,我真的不知道在进行模式匹配时是否要检查两个项目是否属于同一类型.另外,我不确定我的设计是实现我想要做的最好的方法.

To start to solve my problem, I thought I could put the fields as a list of GameItem option and find out which were Some and see, while iterating in that list, if I could find an item which was of the same type as the one I'm trying to add to the equipment. I have some problems, I don't really know while pattern matching out to check if two items are of the same type. Also, I'm not so sure that my design is the best way to go in order to achieve what I'm want to do.

现在,我有这个片段,显然它没有被编译,我正在寻找一种方法使它不仅可以编译,而且可读性强.

Right now, I'm have this snippet which is obviously not compiling and I'm looking for a way to make it not only compiling but readable also.

我看过这篇文章直接在整个记录上进行了一些模式匹配,但是我想知道是否还有其他方法可以对高阶函数进行匹配?

I've seen this post that does some pattern matching directly on the whole record, but I was wondering if there were any other way to do so with high order functions ?

在文件中具有选项类型条目的模式匹配OCaml

更新1

我忘记了我们可以在多个元素上进行模式匹配,所以我的问题真正出在如何动态验证记录字段中的Some值并能够通过高阶函数来实现.

I forgot we could pattern match on more than a single element, so my problem really becomes about how to validate dynamically the Some values in the fields of my record and be able to do it via high order functions.

//更新了函数sendBackToInventory

// Updated function sendBackToInventory

member x.sendBackToInventory (gi: GameItem) =
        let mutable itemFound = false
        let equipmentFields = [ Protection(x.Hat.Value, 0) ; Protection(x.Armor.Value,0); Protection(x.Legs.Value,0); Protection(x.Hands.Value,0); Protection(x.Ring.Value,0); Protection(x.Shield.Value,0) ]
        equipmentFields 
        |> List.iter(fun item ->   
            match item, gi with 
            | (:? Consummable as c1, :? Consummable as c2)  -> 
            | (:? Protection as p1, :? Protection as p2)  -> 
            | (:? Weaponry as w1, :? Weaponry as w2)  -> 
            // match oItem with 
            // | Some item -> 
            //     if item = gi then 
            //         itemFound <- true 
            //         x.InventoryManager <! AddSingleItem gi
            //     else 
            //         ()
            // | None -> ()
        )

更新2

通过添加一个新的有区别的联合来表示它可能是游戏项目的类别,我认为这与领域模型有点多余,我以某种方式克服了我的问题,但这不是我的行为想要保留……不应该有很多样板代码,但是我至少希望有该功能.

By adding a new discriminated union to represent the category of game item it could be, which makes it, in my opinion, a bit redundant with the domain model, I've overcame my issue somehow but it's not a behaviour that I'd like to keep... There's a lot of boilerplate code that shouldn't be there, but I want at least the functionality to be there.

    type ItemCategory = 
        | Weapon 
        | Shield 
        | Loot 
        | Head 
        | Body 
        | Pant 
        | Finger 
        | Hand

    type Equipment = {
        Hat :   GameItem option
        Armor : GameItem option
        Legs  : GameItem option
        Gloves : GameItem option
        Ring  : GameItem option
        Weapon : GameItem option
        Shield : GameItem option
        Loot1  : GameItem option
        Loot2  : GameItem option 
        Loot3  : GameItem option 
        InventoryManager : IActorRef
    }
    with 
        member x.canAddMoreLoot() = 
            not (x.Loot1.IsSome && x.Loot2.IsSome && x.Loot3.IsSome)

        member x.putItemInEquipment 
            (gi: GameItem) 
            (cat: ItemCategory) = 
            let mutable equipment = x 
            match cat with 
            | Head -> 
                 equipment <-  { x with Hat = Some gi } 
                 match x.Hat with 
                 | None ->  ()
                 | Some h ->  x.InventoryManager <! AddItem h

            | Weapon -> 
                 equipment <- { x with Weapon = Some gi } 
                 match x.Weapon with 
                 | None -> () 
                 | Some w -> x.InventoryManager <! AddItem w

            | Shield -> 
                 equipment <- { x with Weapon = Some gi } 
                 match x.Shield with 
                 | None -> () 
                 | Some sh -> x.InventoryManager <! AddItem sh

            | Loot -> 
                 if not (x.canAddMoreLoot()) then x.InventoryManager <! AddItem gi
                 else 
                    match x.Loot1 with 
                    | Some l -> 
                        match x.Loot2 with 
                        | Some l -> equipment <- { x with Loot3 = Some gi } 
                        | None -> equipment <- { x with Loot2 = Some gi }
                    | None -> equipment <- { x with Loot1 = Some gi } 

            | Finger -> 
                equipment <- { x with Ring = Some gi } 
                match x.Ring with
                | None -> () 
                | Some r -> x.InventoryManager <! AddItem r 

            | Body -> 
                equipment <- { x with Armor = Some gi } 
                match x.Armor with 
                | None -> () 
                | Some a -> x.InventoryManager <! AddItem a 
            | Pant ->
                equipment <- { x with Legs = Some gi } 
                match x.Legs with 
                | None -> () 
                | Some l -> x.InventoryManager <! AddItem l 
            | Hand -> 
                equipment <- { x with Gloves = Some gi } 
                match x.Gloves with 
                | None -> () 
                | Some g -> x.InventoryManager <! AddItem g 
            equipment

推荐答案

我已经从您的原始更新中获取了代码,并对其进行了一些微调,以纠正您犯的一些错误.我将稍稍谈谈这些错误,但首先,让我们看一下更正后的代码:

I've taken the code from your original update and tweaked it a bit to correct a couple of mistakes you made. I'll talk about those mistakes in a bit, but first, let's look at the corrected code:

type ConsumableItem =
    | HealthPotion
    | ManaPotion 

type Weaponry = 
    | Sword 
    | Spear 
    | BattleAxe 

type CharacterProtection = 
    | Helmet 
    | Gloves 
    | Boots 

type GameItem = 
    | Consumable of ConsumableItem
    | Weapon of Weaponry
    | Protection of CharacterProtection

type Equipment = { 
     Helmet : CharacterProtection option 
     Weapon : Weaponry option
     Boot   : CharacterProtection option
     Hands  : CharacterProtection option 
     Loot1  : ConsumableItem option
     Loot2  : ConsumableItem option
}

// type Inventory omitted for space

首先,请注意,我更正了ConsumableItem的拼写.它本来可以编译的很好,所以我不会在此花费更多的时间.请注意,拼写发生了变化,因此,如果您将此答案中的任何内容复制并粘贴到您的代码中,就会知道适当地调整了拼写.

First, note that I corrected the spelling of ConsumableItem. It would have compiled just fine, so I won't spend more time on this. Just be aware that the spelling changed, so that if you copy and paste anything from this answer into your code, you'll know to adjust the spelling as appropriate.

第二,我从您的GameItem DU中删除了* int.在我看来,这属于另一种类型,例如称为ItemStack的单例DU:

Second, I removed the * int from your GameItem DU. It seems to me that that belongs in a different type, say a single-case DU called ItemStack:

type ItemStack = ItemStack of item:GameItem * count:int

第三,我调整了您的Equipment记录的类型,该记录不会进行编译.歧视性工会的情况不是类型.他们是构造函数. DU本身就是一种类型. (有关更多信息,请参见 http://fsharpforfunandprofit.com/posts/discriminated-unions/,尤其是标题为构造并集类型的值"的部分).因此,您不能拥有类型为Protection的字段Helmet,因为名称Protection并不引用类型.该字段必须为GameItem类型.但是,您实际上要执行的操作是确保Helmet字段仅保留保护项-因此使它成为CharacterProtection类型是可行的.这仍然不能完全达到使非法状态无法代表的水平,因为您可以将靴子放在Helmet插槽中.但是,要完全使非法状态无法代表,您最终会遇到以下情况:

Third, I adjusted the types of your Equipment record, which would not have compiled. The cases of a discriminated union are not types; they are constructors. The DU itself is a type. (For more information, see http://fsharpforfunandprofit.com/posts/discriminated-unions/ and particularly the section titled "Constructing a value of a union type"). So you can't have a field Helmet of type Protection, because the name Protection does not refer to a type. The field would have to be of type GameItem. But what you're actually trying to do is ensure that the Helmet field will only ever hold a protection item — so making it of type CharacterProtection works. That still doesn't quite reach the level of making illegal states unrepresentable, because you could put boots in the Helmet slot. But to fully make illegal states unrepresentable, you'd end up with something like this:

type Helmet = Helmet of armorValue:int // Plus any other properties you want
type Boots  = Boots  of armorValue:int
type Gloves = Gloves of armorValue:int

type CharacterProtection =
    | Helmet of Helmet
    | Gloves of Gloves
    | Boots  of Boots

type Equipment = {
    Head  : Helmet option
    Feet  : Boots option
    Hands : Gloves option
    // And so on for weapon, potions, etc.
}

我将其称为复杂"游戏模型,并将您的原始代码(包含我的修正)称为简单"游戏模型.两者都是很有价值的,这仅取决于代码的最终生成位置.

I'll call that the "complex" game model, and your original code (with my fixes) I'll call the "simple" game model. Both can be valuable, it just depends on where you end up with your code.

好的,现在让我们忘记我刚刚向您展示的复杂"游戏模型,然后回到简单"游戏模型.您要解决的问题是,当玩家从商店购买头盔时,如果他尚未装备头盔,则想自动装备该头盔.如果他确实装备了头盔,则您希望将购买的头盔放入包中,并让他稍后再装备. (或者,您可以给他提供自动装备他刚购买的头盔的选项,将以前装备的头盔放入袋中.)

Okay, now let's forget about the "complex" game model I just showed you and go back to the "simple" game model. The problem you're trying to solve is that when the player buys, say, a helmet from the store, you want to auto-equip the helmet if he didn't have a helmet already equipped. If he did have a helmet equipped, you want the purchased helmet to go in the bag and let him equip it later. (Alternatively, you could offer him the option to auto-equip the helmet that he's just bought, putting the previously-equipped helmet in the bag.)

那么,您可能想做的就是这样:

What you probably want to do, then, is something like this:

let addToInventory newItem inventory =
    let newBag = inventory.Bag |> Array.append [|newItem|]
    { inventory with Bag = newBag }

let playerWantsToAutoEquip newItem =
    // In real game, you'd pop up a yes/no question for the player to click on
    printfn "Do you want to auto-equip your purchase?"
    printfn "Actually, don't bother answering; I'll just assume you said yes."
    true

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 equipPurchasedWeapon newItem (inventory,equipment) =
    // No need to match against newItem here; weapons are simpler
    match equipment.Weapon with
    | None ->
        let newEquipment = { equipment with Weapon = Some newItem }
        (inventory,newEquipment)
    | Some oldWeapon
        if (playerWantsToAutoEquip newItem) then
            let newEquipment = { equipment with Weapon = Some newItem }
            let newInventory = inventory |> addToInventory oldWeapon
            (newInventory,newEquipment)
        else
            let newInventory = inventory |> addToInventory newItem
            (newInventory,equipment)

// I'll skip defining equipPurchasedConsumable since it's very similar

let equipPurchasedItem gameItem (inventory,equipment) =
    let funcToCall =
        match gameItem with
        | Consumable of potion -> equipPurchasedConsumable potion
        | Weapon of weapon     -> equipPurchasedWeapon weapon
        | Protection of armor  -> equipPurchasedProtection armor
    (inventory,equipment) |> funcToCall

现在,这些功能仍有一些冗余. equipPurchasedPotion中的匹配项看起来非常非常相似.因此,让我们抽象出我们可以做到的:

Now, there's still some redundancy in those functions. The match cases in equipPurchasedPotion all look very, very similar. So let's abstract out what we can:

let equipHelmet newHelm equipment =
    { equipment with Helmet = Some newHelm }
let getOldHelmet equipment = equipment.Helmet

let equipGloves newGloves equipment =
    { equipment with Hands = Some newGloves }
let getOldGloves equipment = equipment.Hands

let equipBoots newBoots equipment =
    { equipment with Boots = Some newBoots }
let getOldBoots equipment = equipment.Boots

let equipWeapon newWeapon equipment =
    { equipment with Weapon = Some newWeapon }
let getOldWeapon equipment = equipment.Weapon

let genericEquipFunction (getFunc,equipFunc) newItem equipment =
    let oldItem = equipment |> getFunc
    let newEquipment = equipment |> equipFunc newItem
    match oldItem with
    | None -> (None,newEquipment)
    | Some _ ->
        if playerWantsToAutoEquip newItem then
            (oldItem,newEquipment)
        else
            (newItem,equipment)

let equipPurchasedProtection newItem (inventory,equipment) =
    let equipFunction =
        match newItem with
        | Helmet -> genericEquipFunction (getOldHelmet,equipHelmet)
        | Gloves -> genericEquipFunction (getOldGloves,equipGloves)
        | Boots  -> genericEquipFunction (getOldBoots, equipBoots)
    let itemForInventory,newEquipment = equipFunction newItem equipment
    match itemForInventory with
    | None -> (inventory,newEquipment)
    | Some item ->
        let newInventory = inventory |> addToInventory item
        (newInventory,newEquipment)

let equipPurchasedWeapon newItem (inventory,equipment) =
    // Only one possible equipFunction for weapons
    let equipFunction = genericEquipFunction (getOldWeapon,equipWeapon)
    let itemForInventory,newEquipment = equipFunction newItem equipment
    match itemForInventory with
    | None -> (inventory,newEquipment)
    | Some item ->
        let newInventory = inventory |> addToInventory item
        (newInventory,newEquipment)

实际上,现在我们可以将equipPurchasedProtectionequipPurchasedWeapon函数组合为一个通用的equipPurchasedItem函数!类型必须更改:现在,newItem参数的类型将为GameItem.但是您仍然可以同时与嵌套DU的多个级别"匹配,就像这样:

In fact, now we're able to combine the equipPurchasedProtection and equipPurchasedWeapon functions into a single generic equipPurchasedItem function! The type has to change: now the newItem parameter is going to be of type GameItem. But you can still match against multiple "levels" of nested DUs at once, like so:

let equipPurchasedItem newItem (inventory,equipment) =
    let equipFunction =
        match newItem with
        | Protection Helmet -> genericEquipFunction (getOldHelmet,equipHelmet)
        | Protection Gloves -> genericEquipFunction (getOldGloves,equipGloves)
        | Protection Boots  -> genericEquipFunction (getOldBoots, equipBoots)
        | Weapon _ -> genericEquipFunction (getOldWeapon,equipWeapon)
        // getOldLoot1 and similar functions not shown. Just pretend they exist.
        | Consumable HealthPotion -> genericEquipFunction (getOldLoot1,equipLoot1)
        | Consumable ManaPotion   -> genericEquipFunction (getOldLoot2,equipLoot2)
    let itemForInventory,newEquipment = equipFunction newItem equipment
    match itemForInventory with
    | None -> (inventory,newEquipment)
    | Some item ->
        let newInventory = inventory |> addToInventory item
        (newInventory,newEquipment)

这种具有通用getset函数,并将它们配对成一个可以传递的元组的技术通常称为镜头".使用镜头通常可以使您编写更多的通用代码,而不必担心如何更新数据结构的特定部分.您可以只编写执行业务逻辑的通用代码,然后告诉镜头这里是应该放入结构中的新数据.我不在乎您是如何执行的,只是以某种方式将其放入其中".这样可以更好地分离关注点:决定做什么的代码与知道如何做到这一点的代码是分开的.使用镜头时,该代码的外观如下:

This technique of having a generic get and set function, and pairing them up in a tuple that you can pass around, is usually called a "lens". Using lenses can often let you write much more generic code that doesn't have to worry about how you update a particular part of your data structures. You can just write generic code that does your business logic, and then tells the lens "Here's the new data that should go into the structure. I don't care how you do it, just put it in there somehow". This allows better separation of concerns: the code that decides what to do is separate from the code that knows how to do it. Here's how that code would have looked using lenses:

type Lens<'r,'a> = getter:('r -> 'a) * setter:('a -> 'r -> 'r)

let equipHelmet newHelm equipment =
    { equipment with Helmet = Some newHelm }
let getOldHelmet equipment = equipment.Helmet
// Convention for lenses is to give them a name that ends with one underscore
let Helmet_ = (getOldHelmet,equipHelmet)
// Now Helmet_ has type Lens<CharacterProtection,Equipment>

let equipGloves newGloves equipment =
    { equipment with Hands = Some newGloves }
let getOldGloves equipment = equipment.Hands
let Gloves_ = (getOldGloves,equipGloves)
// Gloves_ also has type Lens<CharacterProtection,Equipment>

let equipBoots newBoots equipment =
    { equipment with Boots = Some newBoots }
let getOldBoots equipment = equipment.Boots
let Boots_ = (getOldBoots,equipBoots)
// Boots_ also has type Lens<CharacterProtection,Equipment>

let equipWeapon newWeapon equipment =
    { equipment with Weapon = Some newWeapon }
let getOldWeapon equipment = equipment.Weapon
let Weapon_ = (getOldWeapon,equipWeapon)
// Weapon_ has a different type: Lens<Weaponry,Equipment>

// And so on for getOldLoot1,equipLoot1, and so on

let equipWithLens itemLens newItem equipment =
    let oldItem = equipment |> itemLens.getter
    let newEquipment = equipment |> itemLens.setter newItem
    match oldItem with
    | None -> (None,newEquipment)
    | Some _ ->
        if playerWantsToAutoEquip newItem then
            (oldItem,newEquipment)
        else
            (newItem,equipment)

let equipPurchasedProtection newItem (inventory,equipment) =
    let lens =
        match newItem with
        | Protection Helmet -> Helmet_
        | Protection Gloves -> Gloves_
        | Protection Boots  -> Boots_
        | Weapon -> Weapon_
        | Consumable HealthPotion -> Loot1_
        | Consumable ManaPotion   -> Loot2_
    let itemForInventory,newEquipment = equipWithLens lens newItem equipment
    match itemForInventory with
    | None -> (inventory,newEquipment)
    | Some item ->
        let newInventory = inventory |> addToInventory item
        (newInventory,newEquipment)

我希望足以让您了解如何将此概念应用于戒指,胸甲,腿甲等.

I hope that's enough to give you an idea of how to apply this concept to things like rings, chest armor, leg armor, and so on.

警告:所有这些代码都刚刚输入到StackOverflow编辑窗口中;我没有在F#Interactive中对其进行任何测试.因此可能会有错别字.总体思路应该可行.

WARNING: All of this code was just typed into the StackOverflow edit window; I did NOT test any of it in F# Interactive. So there may be typos. The general idea should work, though.

更新:在我的代码中,我看到了我一贯犯的类型错误.各种设备(头盔,手套等)的吸气剂和吸气剂期望两种不同的价值!例如,对于头盔,吸气剂仅返回equipment.Helmet,即CharacterProtection option.但是设置者正在将Some newItem分配给Helmet字段.这意味着它期望newItemCharacterProtection —但是我编写通用装备功能的方式是,我从设备记录中获取项目,然后将其传递给设置器.所以我得到一个CharacterProtection option,并将其传递给尝试分配Some newItem的设置器-所以我的设置器正在尝试分配一个CharacterProtection option option!糟糕!

UPDATE: Looking through my code, I see a type error that I made consistently throughout. The getters and setters for the various kinds of equipment (helmet, gloves, etc.) are expecting two different kinds of values! For example, for helmets, the getter is just returning equipment.Helmet, which is a CharacterProtection option. But the setter is assigning Some newItem to the Helmet field. That means that it's expecting newItem to be a CharacterProtection — but the way I wrote the generic equip functions, I'm getting the item from the equipment record and then passing that to the setter. So I'm getting a CharacterProtection option, and passing that to the setter which tries to assign Some newItem — so my setter is trying to assign a CharacterProtection option option! Oops.

要解决此错误,请使用所有的setter函数,并从其中删除Some.然后他们期望的类型将不是CharacterProtection,而是CharacterProtection option-就像吸气剂一样. 这是关键:获取者和设置者应该期望的是相同类型.

To fix this mistake, take all the setter functions and remove the Some from them. Then the type they're expecting won't be a CharacterProtection, it'll be a CharacterProtection option — just like the getters. That's the key: the getters and setters should be expecting the same type.

更新2 :如所承诺的,我将讨论一下设计.让我们看一下Helmet_镜头是否是记录的成员,以及它是函数的情况.

UPDATE 2: As promised, I'll discuss design a little bit. Let's look at what the Helmet_ lens looks like if it's a member of the record, and what it looks like if it's a function.

type Equipment = { 
     Helmet : CharacterProtection option 
     Weapon : Weaponry option
     Boot   : CharacterProtection option
     Hands  : CharacterProtection option 
     Loot1  : ConsumableItem option
     Loot2  : ConsumableItem option }
     with
        member x.getHelmet () = x.Helmet
        member x.equipHelmet newHelm = { x with Helmet = newHelm }
        member x.Helmet_ = (x.getHelmet, x.equipHelmet)

let getHelmetFun e = e.Helmet
let equipHelmetFun newHelm e = { e with Helmet = newHelm }
let HelmetFun_ = (getHelmetFun, equipHelmetFun)

到目前为止,唯一可见的区别是外部函数需要一个显式参数,而实例方法将其嵌入".这就是实例方法的重点,所以这不足为奇.但是,让我们看看将这两种不同的镜头实现用作其他功能的参数是什么:

So far, the only visible difference is that the external functions need an explicit parameter, whereas the instance methods have it "baked in". That's the point of instance methods, so this is no surprise. But let's look at what it's like to use these two different lens implementations as parameters to some other function:

let getWithInstanceLens lens =
    let getter = fst lens
    getter ()

let getWithExternalLens lens record =
    let getter = fst lens
    getter record

再次,这里没有真正的惊喜.作为实例变量的镜头将其实例放入",因此我们不需要向其传递任何参数.定义为外部功能的镜头没有任何烘焙"记录,因此需要传递给它.请注意,这是一个完全通用的函数(两者都是完全通用的),这就是为什么我将参数命名为record而不是equipment的原因:它可以处理任何镜头和匹配的记录(以及getWithInstanceLens可以处理实例被吸气"到吸气剂中的任何镜头.

Again, no real surprises here. The lens that's an instance variable has its instance "baked in", so we don't need to pass it any parameters. The lens that was defined as an external function doesn't have any "baked in" record, so it needs to be passed one. Note that it's a completely generic function (both of these are completely generic), which is why I named the parameter record instead of equipment: it can handle any lens and matching record (and the getWithInstanceLens can handle any lens whose instance is "baked in" to the getter).

但是,当我们尝试使用这些参数作为List.map的参数时,我们发现了一些有趣的东西.让我们看一下代码,然后再讨论.

But when we try to use these as parameters to, say, List.map, we discover something interesting. Let's look at the code, then talk about it.

let sampleEquipment = { Helmet = Some Helmet
                        Weapon = Some BattleAxe
                        Boot   = Some Boots
                        Hands  = Some Gloves
                        Loot1  = Some HealthPotion
                        Loot2  = None }

let noHelm = { sampleEquipment with Helmet = None }
let noBoots = { noHelm with Boot = None }
let noWeapon = { noBoots with Weapon = None }

let progressivelyWorseEquipment = [ sampleEquipment; noHelm; noBoots; noWeapon ]

let demoExternal equipmentList =
    equipmentList
    |> List.map (getWithExternalLens HelmetFun_)

let demoInstance equipmentList =
    equipmentList
    |> List.map (fun x -> getWithInstanceLens x.Helmet_)

demoExternal progressivelyWorseEquipment
demoInstance progressivelyWorseEquipment
// Both return [Some Helmet; None; None; None]

我们注意到的第一件事是,由于实例透镜具有其实例化"的实例,因此将其作为诸如List.map之类的高阶函数的参数传递实际上有点难看.我们必须显式构造一个lambda来调用它,并且我们无法利用F#的出色的部分应用程序语法.但是还有另一个问题.除非您实际将代码复制到F#IDE中,否则它不会变得很明显-但是此版本的demoInstance无法编译!当我说两个函数都返回一个值时,我撒谎了.实际上,F#编译器抱怨demoInstance中的x.Helmet_表达式,并产生以下错误消息:

The first thing we notice is that because the instance lens has its instance "baked in", it's actually slightly uglier to pass it as an argument to higher-order functions like List.map. We have to explicitly construct a lambda to call it, and we can't take advantage of F#'s nice partial-application syntax. But there's another problem. It won't become obvious until you actually copy this code into your F# IDE — but this version of demoInstance won't compile! I lied when I said that both functions return a value; in fact, the F# compiler complains about the x.Helmet_ expression in demoInstance, producing the following error message:

根据此程序点之前的信息查找不确定类型的对象.在此程序指向之前可能需要类型注释,以约束对象的类型.这样可以解决查找问题.

Lookup on object of indeterminate type based on information prior to this program point. A type annotation may be needed prior to this program point to constrain the type of the object. This may allow the lookup to be resolved.

使用demoInstance函数,F#的类型推断无法帮助我们!此时,它所知道的只是equipmentList some 类型的列表(如果将鼠标悬停在它上面,则工具提示将显示类型为'a list),并且未知的'a类型必须具有一个名为Helmet_的实例.但是F#编译器在这一点上还不足以产生正确的CIL字节码-归根结底,这是它的主要工作. Helmet_实例可能有许多不同的类,因此它必须询问您更多信息.因此,您必须在函数上提供一个显式类型注释,该注释现在看起来像这样:

With the demoInstance function, F#'s type inference can't help us out! All it knows at that point is that the equipmentList is a list of some type (if you hover over it in your IDE, the tooltip will show you that it's of type 'a list), and that the unknown 'a type must have an instance called Helmet_. But the F# compiler doesn't know enough at this point to produce correct CIL bytecode — and at the end of the day, that's its main job. There could be many different classes with a Helmet_ instance, so it has to ask you for more information. And so you have to provide an explicit type annotation on your function, which now looks like this:

let demoInstance (equipmentList : Equipment list) =
    equipmentList
    |> List.map (fun x -> getWithInstanceLens x.Helmet_)

为了对比,这又是demoExternal函数:

For contrast, here's the demoExternal function again:

let demoExternal equipmentList =
    equipmentList
    |> List.map (getWithExternalLens HelmetFun_)

如您所见,使用外部函数要干净得多.

As you can see, it's much cleaner to use external functions.

另一方面,在记录定义中使用 static 方法可以使您两全其美:镜头与记录类型和使用它们的方式与使用外部函数几乎完全相同:

On the other hand, using static methods in your record definition gets you the best of both worlds, pretty much: the lenses are strongly associated with the record type, and the way you use them is pretty much identical to using external functions:

type Equipment = {
     Helmet : CharacterProtection option
     Weapon : Weaponry option
     Boot   : CharacterProtection option
     Hands  : CharacterProtection option
     Loot1  : ConsumableItem option
     Loot2  : ConsumableItem option }
     with
        static member getHelmet x = x.Helmet
        static member equipHelmet newHelm x = { x with Helmet = newHelm }
        static member Helmet_ = (Equipment.getHelmet, Equipment.equipHelmet)

let demoStatic equipmentList =
    equipmentList
    |> List.map (getWithExternalLens Equipment.Helmet_)

这里,在易用性方面没有缺点;这看起来与外部功能"示例几乎相同.就个人而言,我更喜欢这种方法,或者在模块中定义它们"方法:

Here, there's no disadvantage in terms of ease of use; this looks pretty much identical to the "external functions" example. Personally, I'd prefer either this approach, or else the "define them in a module" approach:

module EquipmentLenses =
    let getHelmet e = e.Helmet
    let equipHelmet hewNelm e = { e with Helmet = newHelm }
    let Helmet_ = (getHelmet,equipHelmet)

let demoModule equipmentList =
    equipmentList
    |> List.map (getWithExternalLens EquipmentLenses.Helmet_)

在静态方法和外部模块之间,这实际上只是个人喜好问题-您更喜欢如何组织代码.任一个都是很好的解决方案.

Between static methods and external modules, it's really just a matter of personal preference — how you prefer to organize your code. Either one is a good solution.

这篇关于如何使用高阶函数来过滤和匹配记录中的选项字段的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆