派生类型的模式匹配对于 F# 来说是惯用的吗?

Posted

技术标签:

【中文标题】派生类型的模式匹配对于 F# 来说是惯用的吗?【英文标题】:Is pattern matching on derived types idiomatic for F#? 【发布时间】:2016-07-31 11:16:42 【问题描述】:

我想以最惯用的方式实现以下内容

玩家在地图上。

    如果他与箭处于相同位置,他会受到1点伤害

    如果他与一个生物处于同一位置,他会受到等于该生物生命值的伤害

    如果他与硬币处于相同的位置,他得到1美元

    如果他与药物处于相同位置,他会治愈 1

这是一个为交互而写的存根:

open System
[<AbstractClass>]
type ActorBase(x,y,symbol)=
    member this.X:int=x
    member this.Y:int=y
    member this.Symbol:char=symbol

type Medication(x,y)=
    inherit ActorBase(x,y,'♥')
type Coin(x,y)=
    inherit ActorBase(x,y,'$') 

type Arrow(x,y,symbol,targetX,targetY) =
    inherit ActorBase(x,y,symbol)
    member this.TargetX=targetX
    member this.TargetY=targetY

[<AbstractClass>]
type CreatureBase(x,y,symbol,hp) =
    inherit ActorBase(x,y,symbol)
    member this.HP:int=hp

type Player(x,y,hp, score) =
    inherit CreatureBase(x,y,'@',hp)
    member this.Score = score
type Zombie(x,y,hp,targetX,targetY) =
    inherit CreatureBase(x,y,'z',hp)
    member this.TargetX=targetX
    member this.TargetY=targetY

let playerInteraction (player:Player) (otherActor:#ActorBase):unit =
    printfn "Interacting with %c" otherActor.Symbol
    match (otherActor :> ActorBase) with
            | :? CreatureBase as creature -> printfn "Player is hit by %d by creature %A" (creature.HP) creature
            | :? Arrow -> printfn "Player is hit by 1 by arrow" 
            | :? Coin -> printfn "Player got 1$" 
            | :? Medication -> printfn "Player is healed by 1"
            | _ -> printfn "Interaction is not recognized" 

let otherActorsWithSamePosition (actor:#ActorBase) =
    seq
        yield new Zombie(0,0,3,1,1) :> ActorBase
        yield new Zombie(0,1,3,1,1) :> ActorBase
        yield new Arrow(0,0,'/',1,1) :> ActorBase
        yield new Coin(0,0) :> ActorBase
        yield new Medication(0,0) :> ActorBase
     
        |> Seq.where(fun a -> a.X=actor.X && a.Y=actor.Y)
[<EntryPoint>]
let main argv = 
    let player = new Player(0,0,15,0)
    for actor in (otherActorsWithSamePosition player) do
        playerInteraction player actor
    Console.ReadLine() |> ignore
    0

1) 类和继承是否要在 F# 中使用?或者它们只是为了与.Net 兼容?我是否应该使用记录来代替,如果可以,如何使用?

2) 在 C# 中切换类型被认为是一种不好的做法。 F#也一样吗?如果是,我应该写什么而不是otherActorsWithSamePosition?为从 actor 派生的每个类 X 实现 otherXsWithSamePosition 看起来不像可扩展的解决方案

更新:

我尝试使用可区分的联合来实现它,但没有设法编译:

type IActor =
    abstract member X:int
    abstract member Y:int
    abstract member Symbol:char
type IDamagable =
    abstract member Damaged:int->unit
type IDamaging =
    abstract member Damage:int
type Player =
    
        X:int
        Y:int
        HP:int
        Score:int
    
    interface IActor with
        member this.X=this.X
        member this.Y=this.Y
        member this.Symbol='@'
    interface IDamagable with
        member this.Damaged damage = printfn "The player is damaged by %d" damage
    interface IDamaging with
        member this.Damage = this.HP
type Coin =
    
        X:int
        Y:int
    
    interface IActor with
        member this.X=this.X
        member this.Y=this.Y
        member this.Symbol='$'
type Medication =
    
        X:int
        Y:int
    
    interface IActor with
        member this.X=this.X
        member this.Y=this.Y
        member this.Symbol='♥'
type Arrow =
    
        X:int
        Y:int
        DestinationX:int
        DestinationY:int
        Symbol:char
    
    interface IActor with
        member this.X=this.X
        member this.Y=this.Y
        member this.Symbol=this.Symbol
    interface IDamaging with
        member this.Damage = 1
type Zombie =
    
        X:int
        Y:int
        DestinationX:int
        DestinationY:int
        HP:int
    
    interface IActor with
        member this.X=this.X
        member this.Y=this.Y
        member this.Symbol='z'
    interface IDamaging with
        member this.Damage = this.HP
type Actor =
    |Player of Player
    |Coin of Coin
    |Zombie of Zombie
    |Medication of Medication
    |Arrow of Arrow
let otherActorsWithSamePosition (actor:Actor) =
    seq
        yield Zombie X=0;Y=0; HP=3;DestinationX=1;DestinationY=1
        yield Zombie X=0;Y=1; HP=3;DestinationX=1;DestinationY=1
        yield Arrow X=0;Y=0; Symbol='/';DestinationX=1;DestinationY=1
        yield Coin X=0;Y=0
        yield Medication X=0;Y=0
     
        //Cannot cast to interface
        |> Seq.where(fun a -> (a:>IActor).X=actor.X && (a:>IActor).Y=actor.Y)
let playerInteraction player (otherActor:Actor) =
    match otherActor with
            | Coin coin -> printfn "Player got 1$" 
            | Medication medication -> printfn "Player is healed by 1"
            //Cannot check this
            | :?IDamaging as damaging -> (player:>IDamagable).Damaged(damaging.Damage)

[<EntryPoint>]
let main argv = 
    let player = Player X=0;Y=0;HP=15;Score=0
    for actor in (otherActorsWithSamePosition player) do
        playerInteraction player actor
    Console.ReadLine() |> ignore
    0

问题:

1)更重要的是:

我无法对现有记录进行有区别的联合

Actor =
    | Medication x:int;y:int;symbol:char 

引发关于不推荐使用的构造的错误

type Medication = x:int;y:int;symbol:char
Actor =
        | Medication

考虑MedicationActor.Medication不同的类型

我使用了一个相当丑陋的结构

type Medication = x:int;y:int;symbol:char
Actor =
    | Medication of Medication

但它阻止我在接口上匹配。

2) F# 中没有隐式接口实现。这条鳕鱼已经有很多样板元素,比如'member this.X=this.X'。使用比“IActor”更复杂的东西会产生越来越多的问题。

您能否提供一个在 F# 中正确使用可区分联合参数的命名参数的示例?在这种情况下有帮助吗?

【问题讨论】:

这是一个更适合 codereview.se 的问题 我认为这两种方法都不一定不好(但请注意,当您可以依靠编译器告诉您错过了一个可能的选项时,模式匹配是最有效的——联合可能会更好例如,选择而不是类)。但是将两者混合是可疑的——如果你使用类,为什么不使用虚拟方法呢?如果您使用函数+记录,为什么要使用类? 您可以很好地在联合中使用记录 - 因此您将像往常一样命名参数(包括函数,如有必要)。但是您在这里确实遗漏了一个更大的观点-您仍在尝试构建一个承载行为的继承层次结构,而您如何做到这一点并不重要。相反,如今通常的方法是使用组合并从构建块(行为等)中构建参与者,而不是通过继承。一旦处理了行为,交互就很简单了——当 Damagingable 与 Damaging 交互时,你并不关心谁是玩家,谁是箭头。 Are classes and inheritance meant to be used in F#? Or are they just for compatibility with .Net? 答案非常主观。我知道有人会强烈同意这一点,而我则采取硬币的另一面并尽可能多地进行构图。当需要兼容性或进行第一次翻译时,我将使用类和继承,但是一旦我的代码工作起来,我就会开始考虑共性并发挥函数式编程而不是 OO 设计的力量。 @user2136963 你可以这样使用:type Actor = | Medication of x:int * y:int * symbol:char 【参考方案1】:

派生类型的模式匹配对于 F# 来说是惯用的吗?

我会说不,因为您有一个基本的谬误,即对象层次结构中的类型代码是惯用的 F#。

我不认为类型的对象层次结构是惯用的 F# 或功能性的。所以这个问题是无效的。我从历史的角度来看 F# 来自 ML 和 OCaml,而不是来自 OO 方面。正如我在编写函数式代码时总是建议的那样,忘记你对 OO 的了解,因为它只会让你走上混乱的道路。如果您必须与 OO 交互,那么您将不得不硬着头皮但尽可能将 OO 排除在外。

类和继承是否要在 F# 中使用? 还是它们只是为了与 .Net 兼容?

如果您查看 F# classes 上的 MSDN 文章部分下 When to Use Classes, Unions, Records, and Structures你会看到

鉴于可供选择的种类繁多,您需要有一个好的 了解每种类型的设计目的是为了选择 适合特定情况的类型。课程专为 在面向对象的编程上下文中使用。面向对象 编程是应用程序中使用的主要范式 为 .NET Framework 编写。如果您的 F# 代码必须紧密配合 使用 .NET Framework 或其他面向对象的库,以及 特别是如果您必须从面向对象的类型系统扩展 例如 UI 库,类可能是合适的。

如果您没有与面向对象的代码紧密互操作,或者如果 您正在编写自包含并因此受到保护的代码 由于与面向对象代码的频繁交互,您应该 考虑使用记录和有区别的工会。一个,好吧 深思熟虑的歧视性联合,以及适当的模式 匹配代码,通常可以用作对象的更简单替代方案 等级制度。有关受歧视工会的更多信息,请参阅 有区别的工会 (F#)。

我应该改用记录吗?如果可以,如何使用?

首先不要使用有区别的联合,然后如果数据变得更复杂,请查看记录。在某些情况下,我经常使用记录,但大多数时候不使用。这是一个it depends 的问题。

记录的优点是比类更简单,但记录 当一个类型的需求超出了可以满足的范围时是不合适的 用它们的简单性来完成。记录基本简单 值的聚合,没有可以执行的单独构造函数 自定义操作,没有隐藏字段,也没有继承或 接口实现。虽然成员如属性和 可以将方法添加到记录中以使其行为更加复杂, 存储在记录中的字段仍然是值的简单聚合。 有关记录的详细信息,请参阅记录 (F#)。

结构对于小型数据聚合也很有用,但它们 与类和记录的不同之处在于它们是 .NET 值类型。 类和记录是 .NET 引用类型。价值的语义 类型和引用类型的不同之处在于传递值类型 按价值。这意味着当它们被复制时,它们会被逐位复制 作为参数传递或从函数返回。他们也是 存储在堆栈中,或者,如果它们用作字段,则嵌入其中 父对象而不是存放在自己单独的位置上 堆。因此,结构适用于经常 当访问堆的开销是一个问题时访问的数据。 有关结构的详细信息,请参阅结构 (F#)。

.

在 C# 中切换类型被认为是一种不好的做法。 F#也一样吗?

查看Is pattern matching on derived types idiomatic for F#?的答案

怎么做?

namespace Game

type Location = int * int
type TargetLocation = Location
type CurrentLocation = Location
type Symbol = char
type ActorType = CurrentLocation * Symbol
type HitPoints = int
type Health = int
type Money = int
type Creature = ActorType * HitPoints

// Player = Creature * Health * Money
//        = (ActorType * HitPoints) * Health * Money
//        = ((CurrentLocation * Symbol) * HitPoints) * Health * Money
//        = ((Location * Symbol) * HitPoints) * Health * Money
//        = (((int * int) * char) * int) * int * int
type Player = Creature * Health * Money 

type Actor =
    | Medication of ActorType
    | Coin of ActorType
    | Arrow of Creature * TargetLocation    // Had to give arrow hit point damage
    | Zombie of Creature * TargetLocation

module main =

    [<EntryPoint>]
    let main argv = 

        let player = ((((0,0),'p'),15),0,0)  

        let actors : Actor List = 
            [
                 Medication((0,0),'♥'); 
                 Zombie((((3,2),'Z'),3),(0,0)); 
                 Zombie((((5,1),'Z'),3),(0,0)); 
                 Arrow((((4,3),'/'),3),(2,1));
                 Coin((4,2),'$'); 
            ]

        let updatePlayer player (actors : Actor list) : Player =
            let interact (((((x,y),symbol),hitPoints),health,money) : Player) otherActor = 
                match (x,y),otherActor with
                | (playerX,playerY),Zombie((((opponentX,opponentY),symbol),zombieHitPoints),targetLocation) when playerX = opponentX && playerY = opponentY -> 
                    printfn "Player is hit by creature for %i hit points." zombieHitPoints
                    ((((x,y),symbol),hitPoints - zombieHitPoints),health,money)
                | (playerX,playerY),Arrow((((opponentX,opponentY),symbol),arrowHitPoints),targetLocation)  when playerX = opponentX && playerY = opponentY ->  
                    printfn "Player is hit by arrow for %i hit points." arrowHitPoints
                    ((((x,y),symbol),hitPoints - arrowHitPoints),health,money)
                | (playerX,playerY),Coin((opponentX,opponentY),symbol)  when playerX = opponentX && playerY = opponentY ->  
                    printfn "Player got 1$." 
                    ((((x,y),symbol),hitPoints),health,money + 1)
                | (playerX,playerY),Medication((opponentX,opponentY),symbol)  when playerX = opponentX && playerY = opponentY ->  
                    printfn "Player is healed by 1."
                    ((((x,y),symbol),hitPoints),health+1,money)
                | _ ->  
                    // When we use guards in matching, i.e. when clause, F# requires a _ match 
                    ((((x,y),symbol),hitPoints),health,money) 
            let rec updatePlayerInner player actors =
                match actors with
                | actor::t ->
                    let player = interact player actor
                    updatePlayerInner player t
                | [] -> player
            updatePlayerInner player actors

        let rec play player actors =
            let player = updatePlayer player actors
            play player actors

        // Since this is example code the following line will cause a stack overflow.
        // I put it in as an example function to demonstrate how the code can be used.
        // play player actors

        // Test

        let testActors : Actor List = 
            [
                Zombie((((0,0),'Z'),3),(0,0))
                Arrow((((0,0),'/'),3),(2,1))
                Coin((0,0),'$')
                Medication((0,0),'♥')
            ]

        let updatedPlayer = updatePlayer player testActors

        printf "Press any key to exit: "
        System.Console.ReadKey() |> ignore
        printfn ""

        0 // return an integer exit code

由于这不是一款完整的游戏,我做了一些测试来展示玩家与其他演员互动的基础知识。

Player is hit by creature for 3 hit points.
Player is hit by arrow for 3 hit points.
Player got 1$.
Player is healed by 1.

如果您对它的工作原理有任何具体问题,请提出一个新问题并参考此问题。

希望来自 OO 的人会明白为什么我们这些转向函数式编程的人会喜欢它,以及为什么在从头开始编写函数式代码时不应该有任何 OO 的想法。同样,如果您正在与其他 OO 代码进行交互,那么有 OO 的想法就可以了。

【讨论】:

坦克很多!我真的很感激你所做的,但是interact 的实现看起来真的很神秘。我无法想象有人喜欢(((((x,y),symbol),hitPoints),health,money) : Player) 而不是(player:Player)。如果 A|B|C 实现了 A、B 和 C 属性的交集,这将更加优雅 我知道代码并不优雅,甚至不是真正地道的 F#。我试图将其保持在低水平,以便您可以轻松地将其分解为您需要的东西,并且您不必取消分解然后重构以获得您需要的东西。另外(((((x,y),symbol),hitPoints),health,money 只是解构播放器,我可以将名称缩短为一两个字符,并将不必要的属性更改为_,但保留它们以便初学者更容易阅读代码。 实际上,我会使用 Asik 的回答中提到的一些要点,并进行了更多的重构,但是对于初学者来说可能并不容易理解。此外,我可以将所有播放器构造函数移动到 interact 的末尾,将播放器和其他实体的位置移动到单独的结构中,这样交互就不必进行所有检查,转换所有构造函数和析构函数中的元组到 curried 参数,删除所有类型并在 DU 和播放器定义等中仅使用 intchar 等。 另请注意,此代码中没有可变值,它都是不可变的,因此需要interact 返回Player 而不是()。我不知道这个例子为什么会导致堆栈溢出,但也许这周晚些时候,当我有更多时间时,我会检查 IL 代码并找出根本原因。 您应该结合这两个答案,因为 Asik 有一些非常好的观点和想法,并将您的结果作为答案发布在这里并获得一些积分或将其发布到代码审查并获得更多反馈。【参考方案2】:

1) 类和继承是否要在 F# 中使用?或者他们是 只是为了与.Net兼容?灵魂我用记录代替,如果 是的,怎么样? 是的,当以面向对象的方式构建程序是有意义的时候。

OO 定义了一组封闭的操作(接口) 开放的数据集(类)。 FP 定义了对封闭数据集(可区分联合)的开放操作集(函数)。

换句话说,OO 可以很容易地对多种形状的数据实现相同的操作,但很难添加新的操作; FP 可以很容易地对数据执行许多不同的操作,但很难修改您的数据。这些方法是互补的。选择对您的问题最有意义的一个。 F# 的好处是它对两者都有很好的支持。您实际上希望在 F# 中默认为 FP,因为语法更轻巧且更具表现力。

2) 在 C# 中切换类型被认为是一种不好的做法。是不是 F#也一样?如果是,我应该写什么而不是 其他ActorWithSamePosition?实现 otherXsWithSamePosition 为 每个派生自 actor 的 X 类看起来都不像一个可扩展的解决方案

打开类型会导致 OO 中的代码变得脆弱,而这在 F# 中不会改变。你仍然会遇到同样的问题。 然而,在 FP 中打开数据类型是惯用的。如果您使用 DU 而不是类层次结构,那么您将别无选择,只能在数据类型上切换(模式匹配)。这很好,因为编译器可以帮助您解决这个问题,这与 OO 不同。

玩家在地图上。

如果他与箭处于相同位置,他会受到 1 点伤害 如果他与一个生物处于同一位置,他会受到等于该生物生命值的伤害 如果他与硬币处于相同的位置,他会得到 1 美元 如果他与药物处于相同位置,他会治愈 1

首先让我们定义我们的领域模型。我发现有问题的设计的一个方面是您将对象位置存储在演员中并且没有实际的地图对象。我发现一个很好的设计原则是让一个对象只存储它的内在属性,并将外在属性移动到更有意义的地方,使域模型尽可能小。演员的位置不是固有属性。

所以,使用惯用的 F# 类型:

type Player = 
     Hp: int
      Score: int 
type Zombie =
     Hp: int
      TargetLocation: (int*int) option 

type Creature =
| Zombie of Zombie

type Actor =
| Arrow
| Medication
| Creature of Creature
| Coin
| Player of Player

这里遗漏的一个信息是符号,但这实际上只是渲染的一个问题,因此最好放在辅助函数中:

let symbol = function
| Arrow -> '/'
| Medication -> '♥'
| Creature c -> 
    match c with
    | Zombie _ -> 'X'
| Coin -> '$'
| Player _ -> '@'

现在根据您的描述,单个图块上可以有多个演员,我们将把地图表示为 Actor list [][],即演员列表的 2D 地图。

let width, height = 10, 10
let map = Array.init height (fun y -> Array.init width (fun x -> List.empty<Actor>))

// Let's put some things in the world
map.[0].[1] <- [Arrow]
map.[2].[2] <- [Creature(Zombie  Hp = 10; TargetLocation = None )]
map.[0].[0] <- [Player  Hp = 20; Score = 0]

请注意,这不是一种非常实用的方法,因为我们将改变数组而不是创建新数组,但在游戏编程中这很常见,出于明显的性能原因。

现在你的 playerInteraction 函数看起来像这样(实际上是在实现规范而不是打印出字符串):

let applyEffects  Hp = hp; Score = score  actor =
    let originalPlayer =  Hp = hp; Score = score 
    match actor with
    | Arrow ->  originalPlayer with Hp = hp - 1 
    | Coin ->  originalPlayer with Score = score + 1 
    | Medication ->  originalPlayer with Hp = hp + 1 
    | Creature(Zombie z) ->  originalPlayer with Hp = hp - z.Hp 
    | _ -> originalPlayer

这里遗漏的一个问题是:我如何获得玩家的位置?您可以缓存它,或者每次都即时计算它。如果地图很小,这并不慢。这是一个示例函数(未优化,如果您广泛使用 2D 地图,您将希望实现不分配的快速通用迭代器):

let getPlayer: Player * (int * int) =
    let mapIterator = 
        map 
        |> Seq.mapi(fun y row -> 
            row |> Seq.mapi(fun x actors -> actors, (x, y))) 
        |> Seq.collect id
    mapIterator 
    |> Seq.pick(fun (actors, (x, y)) -> 
        actors |> Seq.tryPick(function 
                              | Player p -> Some (p, (x, y)) 
                              | _ -> None))

【讨论】:

你躲过了每一个有位置的演员的找位置。我首先想到的是,在一般情况下它不会这样做,但是查看您的实现我只是认为接口IHitable 可以表示为一个活动模式,它接受Actor。非常感谢您分享这个! “找到每个有位置的演员的位置”?那是什么意思?如果您希望所有演员都在给定位置 (x, y),这很简单:map.[y].[x] 而是寻找给定演员的位置。但是您可以争辩说,由于它不是内在属性,因此演员之间没有区别,只是位置不同。 一般来说,问题在于根据谓词p 查找可区分联合实例的子集,该谓词仅适用于联合的特定情况。您避开了谓词 when instance.X=givenX and instance.Y = givenY 的过滤,解决了特定的联合情况 Player 我们可以过滤actors,例如,通过谓词when instance.HP&lt;3,找出所有可能很快死亡的actors。使用 OOP,我可以检查层次结构中的类是否实现接口 IHitable 并显示 (instance :&gt; IHitable).HP。但是有区别的联合的情况(?不幸的是)不能实现接口。我只是想,我可以定义一个模式(IHitable of HP=int|_) 并通过向它添加更多检查来显式地实现接口。 (一个相当冗长的解决方案)但我不确定活动模式是否有可能有参数,现在要测试它。

以上是关于派生类型的模式匹配对于 F# 来说是惯用的吗?的主要内容,如果未能解决你的问题,请参考以下文章

什么是惯用的 Hamcrest 模式来断言可迭代的每个元素都匹配给定的匹配器?

模式匹配是首选还是惯用 Erlang 中的 case 语句?

SemVer 对于持续交付来说是多余的吗?

c# 中模拟一个模式匹配及匹配值抽取

简单工厂模式

Azure Stream Analytics工作对于小数据来说是昂贵的吗?