如何使用高阶函数来过滤和匹配记录中的选项字段 [英] How to use high order functions to filter and match on options fields in a record
问题描述
因此,需要一些上下文信息才能理解我的问题.在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 ?
更新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)
实际上,现在我们可以将equipPurchasedProtection
和equipPurchasedWeapon
函数组合为一个通用的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)
这种具有通用get
和set
函数,并将它们配对成一个可以传递的元组的技术通常称为镜头".使用镜头通常可以使您编写更多的通用代码,而不必担心如何更新数据结构的特定部分.您可以只编写执行业务逻辑的通用代码,然后告诉镜头这里是应该放入结构中的新数据.我不在乎您是如何执行的,只是以某种方式将其放入其中".这样可以更好地分离关注点:决定做什么的代码与知道如何做到这一点的代码是分开的.使用镜头时,该代码的外观如下:
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
字段.这意味着它期望newItem
是CharacterProtection
—但是我编写通用装备功能的方式是,我从设备记录中获取项目,然后将其传递给设置器.所以我得到一个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屋!