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

时间:2016-04-10 11:46:17

标签: types f# functional-programming idiomatic

我希望以最恰当的方式实现以下内容

玩家在地图上。

  1. 如果他带箭头的位置相同,则会受到1点伤害

  2. 如果他与一个生物处于相同的位置,他的伤害等于生物的hp

  3. 如果他和一枚硬币在同一个位置,他会得到1 $

  4. 如果他与药物处于同一位置,他就会治愈1

  5. 这是为交互编写的存根:

    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派生的每个类otherXsWithSamePosition实现X看起来不像是一个可扩展的解决方案

    更新:

    我试图用一个有区别的联盟实现它,但没有设法编译:

    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#中没有隐式接口imlement。这个鳕鱼已经有很多样板元素,例如&#39;成员this.X = this.X&#39;。有一些比“IActor”更复杂的东西。它会产生越来越多的问题。

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

1 个答案:

答案 0 :(得分:3)

  

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

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

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

  

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

接通类型会导致OO中的代码变弱,而且F#中的代码不会发生变化。你还会遇到同样的问题。 然而,在FP中切换数据类型是惯用的。如果您使用DUs而不是类层次结构,那么除了在数据类型上切换(模式匹配)之外别无选择。这很好,因为编译器可以帮助你,不像OO。

  

玩家在地图上。

     
      
  • 如果他带箭头的位置相同,则会受到1点伤害
  •   
  • 如果他与一个生物处于相同的位置,他的伤害等于生物的hp
  •   
  • 如果他和一枚硬币在同一个位置,他就会获得1美元
  •   
  • 如果他与药物处于同一位置,他会以1
  • 治愈   

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

所以,使用惯用的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))