Это шаблоны на производные типы идиоматических для F#?


Я хочу реализовать следующее наиболее идиоматическим способом

Игрок находится на карте.

  1. Если он находится в одной позиции со стрелой, он получает 1 Урон

  2. Если он находится в одной позиции с существом, он получает урон, равный hp существа

  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) предназначены ли классы и наследование для использования в F#? Или они просто для совместимости с .Net? должен ли я использовать записи вместо этого, и если да, то как?

2) включение типов считается плохой практикой в C#. Это то же самое для F#? Если да, то что я должен написать вместо otherActorsWithSamePosition? Реализация otherXsWithSamePosition для каждого класса X, производного от actor, не выглядит масштабируемым решением

Обновление:

Я попытался реализовать его с помощью дискриминированного Союза, но не смог скомпилировать:

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

Рассматривает Medication и Actor.Medication различные типы

Я использовал довольно уродливую конструкцию

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

Но это мешает мне соответствовать дальше межфазные границы.

2) отсутствие неявных имлементаций интерфейса в F#. Эта треска уже имеет много шаблонных элементов, таких как " член это.Х=это.X'. С чем-то более сложным, чем "IActor", это создаст все больше и больше проблем.

Не могли бы вы привести пример корректного использования именованных параметров параметров дискриминируемых союзов в F#? Помогает ли это в данном случае?

2 4

2 ответа:

- это шаблон соответствия на производные типы идиоматических для F#?

Я бы сказал нет, потому что у вас есть основное заблуждение, что код с типами в иерархии объектов является идиоматическим F#.

Я не считаю иерархии объектов типов, чтобы быть идиоматические F# или функциональной. Так что вопрос является недопустимым. Я рассматриваю историческую перспективу с F#, исходящей от ML и OCaml, в отличие от исходящей со стороны OO. Как я всегда советую при работе с функциональным кодом, забудьте о том, что вы знаете О, как это только приведет вас на путь заблуждения. Если вы должны взаимодействовать с ОО, то вам придется укусить пулю, но оставить ОО, когда это возможно.

Предназначены ли классы и наследование для использования в F#?
Или они просто для совместимости с .Net?

Если вы посмотрите на статью MSDN о классах F# в разделе When to Use Classes, Unions, Records, and Structures вы увидите

Учитывая разнообразие типов на выбор, вы должны иметь хороший понимание того, для чего каждый тип предназначен для выбора подходящий тип для конкретной ситуации. Классы предназначены для используется в контекстах объектно-ориентированного программирования. Объектно-ориентированный программирование является доминирующей парадигмой, используемой в приложениях, которые написано для платформы .NET Framework. Если ваш F# код должен работать в тесном контакте с помощью .NET Framework или другой объектно-ориентированной библиотеки, а также особенно, если вам приходится расширяться из объектно-ориентированной системы типов например, библиотека пользовательского интерфейса, занятия, вероятно, уместны.

Если вы не тесно взаимодействуете с объектно-ориентированным кодом, или если вы пишете код, который является автономным и поэтому защищенным из-за частого взаимодействия с объектно-ориентированным кодом вы должны рассмотрите возможность использования записей и дискриминируемых союзов. Один, хорошо продуманный дискриминируемый союз, вместе с соответствующим шаблоном соответствующий код часто может использоваться в качестве более простой альтернативы объекту иерархия. За дополнительной информацией о дискриминируемых профсоюзах см. Дискриминируемые Союзы (F#).

.

Должен ли я использовать записи вместо этого, и если да, то как?

Сначала не используйте дискриминированное объединение, а затем, если данные становятся более сложными, взгляните на записи. В некоторых ситуациях я часто использую записи, но в большинстве случаев этого не делаю. Это вопрос it depends.

Записи имеют то преимущество, что они проще классов, но записи не уместны, когда требования тип превышают то, что может быть выполненный с их простотой. Записи в основном просты агрегаты значений, без отдельных конструкторов, которые могут выполнять настраиваемые действия, без скрытых полей, без наследования или реализация интерфейса. Хотя такие члены, как свойства и методы могут быть добавлены к записям, чтобы сделать их поведение более сложным, поля, хранящиеся в записи, по-прежнему представляют собой простую совокупность значений. Дополнительные сведения о записях см. В разделе записи (Ф#).

Структуры также полезны для небольших агрегатов данных, но они отличается от классов и записей тем, что они являются типами значений .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.
Если у вас есть конкретные вопросы о том, как это работает, пожалуйста, задайте новый вопрос и вернитесь к этому вопросу. Надеюсь, люди из ОО поймут, почему те из нас, кто перешел на функциональное программирование, наслаждаются им и почему у вас не должно быть никаких мыслей ОО в вашем уме, когда вы делаете функциональный код с нуля. Опять же, если вы взаимодействуете с другими ОО код тогда иметь ОО мысли-это прекрасно.

1) предназначены ли классы и наследование для использования в F#? Или они есть просто для совместимости с .Net? должен ли я использовать записи вместо этого, и, если да, но как? Да, когда имеет смысл структурировать вашу программу в объектно-ориентированном виде.

  • OO определяет замкнутые множества операций (интерфейсов) над открытый набор данных (классов).
  • FP определяет открытые наборы операций (функций) над закрытым набором данных (дискриминируемые объединения).

В другом другими словами, OO позволяет легко реализовать одну и ту же операцию над многими формами данных, но трудно добавлять новые операции; FP позволяет легко реализовать множество различных операций над данными, но трудно модифицировать ваши данные. Эти подходы дополняют друг друга. Выберите тот, который имеет наибольший смысл для вашей проблемы. Хорошая вещь о F# - это то, что он имеет большую поддержку для обоих; вы действительно захотите по умолчанию использовать FP в F#, так как синтаксис легче и выразительнее.

2) включение типов является считается плохой практикой в C#. Это он и есть то же самое для F#? Если да, то что я должен написать вместо этого другие акторы в таком положении? Реализация otherXsWithSamePosition для каждый класс X, производный от actor, не выглядит масштабируемым решением

Включение типов приводит к хрупкому коду в OO, и это не меняется в F#. Вы все равно столкнетесь с теми же проблемами. Однако включение типов данных в FP является идиоматическим. Если вместо иерархий классов вы используете DUs, то у вас не будет выбора но для переключения (pattern-match) на типы данных. И это хорошо, потому что компилятор там, чтобы помочь вам с этим, в отличие от OO.

Игрок находится на карте.

  • если он находится в одной позиции со стрелой, он получает 1 Урон
  • если он находится в одной позиции с существом, он получает урон, равный hp существа
  • Если он находится в том же положении с монетой, он получает 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))