F# 的派生类型上的模式匹配是惯用的吗?

Is pattern matching on derived types idiomatic for F#?

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

玩家在地图上。

  1. 如果他和箭在同一位置,他受到1点伤害

  2. 如果他和一个生物在同一个位置,他受到等于该生物生命值的伤害

  3. 如果他和硬币处于同一位置,他得到1$

  4. 如果他处于同一位置并服药,他会恢复 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) classes 和继承是要在 F# 中使用吗?或者它们只是为了与 .Net 兼容?我是否应该改用记录,如果是,如何?

2) 在 C# 中切换类型被认为是一种不好的做法。 F# 也一样吗?如果是,我应该写什么而不是 otherActorsWithSamePosition?为从 actor 派生的每个 class 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# 中没有隐式接口实现。这个 cod 已经有很多样板元素,比如 'member this.X=this.X'。比 'IActor' 更复杂的东西会产生越来越多的问题。

能否提供一个在 F# 中正确使用 Discriminated Unions 参数的命名参数的示例?在这种情况下有帮助吗?

Is pattern matching on derived types idiomatic for F#?

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

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

Are classes and inheritance meant to be used in F#?
Or are they just for compatibility with .Net?

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

Given the variety of types to choose from, you need to have a good understanding of what each type is designed for to select the appropriate type for a particular situation. Classes are designed for use in object-oriented programming contexts. Object-oriented programming is the dominant paradigm used in applications that are written for the .NET Framework. If your F# code has to work closely with the .NET Framework or another object-oriented library, and especially if you have to extend from an object-oriented type system such as a UI library, classes are probably appropriate.

If you are not interoperating closely with object-oriented code, or if you are writing code that is self-contained and therefore protected from frequent interaction with object-oriented code, you should consider using records and discriminated unions. A single, well thought–out discriminated union, together with appropriate pattern matching code, can often be used as a simpler alternative to an object hierarchy. For more information about discriminated unions, see Discriminated Unions (F#).

Should I use records instead, and, if yes, how?

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

Records have the advantage of being simpler than classes, but records are not appropriate when the demands of a type exceed what can be accomplished with their simplicity. Records are basically simple aggregates of values, without separate constructors that can perform custom actions, without hidden fields, and without inheritance or interface implementations. Although members such as properties and methods can be added to records to make their behavior more complex, the fields stored in a record are still a simple aggregate of values. For more information about records, see Records (F#).

Structures are also useful for small aggregates of data, but they differ from classes and records in that they are .NET value types. Classes and records are .NET reference types. The semantics of value types and reference types are different in that value types are passed by value. This means that they are copied bit for bit when they are passed as a parameter or returned from a function. They are also stored on the stack or, if they are used as a field, embedded inside the parent object instead of stored in their own separate location on the heap. Therefore, structures are appropriate for frequently accessed data when the overhead of accessing the heap is a problem. For more information about structures, see Structures (F#).

.

Switching on types is considered a bad practice in C#. Is it the same for F#?

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

的答案

How?

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 思想就很好。

1) Are classes and inheritance meant to be used in F#? Or are they just for compatibility with .Net? Sould I use records instead, and, if yes, how? Yes, when it makes sense to structure your program in an object-oriented way.

  • OO 定义了封闭的操作集(接口) 打开数据集 (classes)。
  • FP 在封闭数据集(可区分联合)上定义开放的操作集(函数)。

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

2)Switching on types is considered a bad practice in C#. Is it the same for F#? If yes what should I write instead of otherActorsWithSamePosition? Implementing otherXsWithSamePosition for each class X derived from actor doesn't look like a scalable solution

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

A player is on a map.

  • If he is in the same position with an arrow, he gets 1 damage
  • If he is in the same position with a creature, he gets damage equal to hp of creature
  • If he is in the same position with a coin, he gets 1$
  • If he is in the same position with a medication, he heals by 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 [][],即演员列表的二维地图。

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))