Pregunta

I'm trying to wrap my head around F#, and for the most part it's making sense, but people keep saying that every problem can be solved with immutable constructs. But what about objects in a game? They have constantly changing variables (x, y, xspeed, yspeed, score, health etc...)

Lets say I have a player object, defined in C# as:

public class Player extends GameObject
{
    private int score = 0;
    private int x = 0;
    private int y = 0;    
    ... more variables

    public Player()
    {

    }

    public void addScore(int amount)
    {
        score += amount;
    }

    public void addX(int amount)
    {
        x += amount;
    }

    public void addY(int amount)
    {
        y += amount;
    }

    ... more methods
}

This code has variables that are inherently there for being just that: "variables." How would this code translate to immutable code?

¿Fue útil?

Solución

By definition something that is immutable can't change, so in order to update an immutable game object a new object that contains the updates must be created.

type Player(score, x, y) =
    member this.addScore(amount) =
        Player(score + amount, x, y)

    member this.addX(amount) =
        Player(score, x + amount, y)

    member this.addY(amount) =
        Player(score, x, y + amount)

let first = Player(0, 0, 0)
let firstMoved = first.addX(2) 

Passing every variable to the constructor can get unwieldy and slow. To avoid this organize data into different classes. This will allow for the reuse of smaller objects to create bigger ones. Since these objects are immutable, they will always contain the same data and can be reused freely without worry.

type Stats(score, lives) =
    member this.addPoints(points) =
        Stats(score + points, lives)

type Location(x, y) =
    member this.move(x2, y2) =
        Location(x + x2, y + y2)

type Player(stats : Stats, location : Location) =
    member this.addScore(amount) =
        Player(stats.addPoints(amount), location)

    member this.addX(amount) =
        Player(stats, location.move(amount, 0))


let first = Player(Stats(0, 1), Location(0, 0))
let firstMoved = first.addX(2) 

F# also supports mutable data. You can write your original class like this.

type Player() =
    inherit GameObject()

    let mutable score = 0
    let mutable x = 0
    let mutable y = 0

    member this.addScore(amount) =
        score <- score + amount

    member this.addX(amount) =
        x <- x + amount

    member this.addY(amount) =
        y <- y + amount

Otros consejos

You can model you player with a record and then add functions to update them:

type Player = {
    score: int
    x: int
    y: int
}

let addX (player: Player) amount = { player with x = (player.x + amount) }

As others have said, you update properties of an immutable object by creating a new object with updated properties. But let me try to illustrate how this might work with some code.

For simplicity, let's say you are modelling a particle system, where each particle moves accordingly to it's velocity.

Your model comprises of a set of particles that are updated and then rendered to the screen each frame.

In C#, your model implementation might look like this:

public class Model
{
    public class Particle
    {
        public double X { get; set; }
        public double Y { get; set; }
        public double Vx { get; set; }
        public double Vy { get; set; }
    }

    public Model()
    {
        this.particles = /* ... initialize a set of particles ... */
    }

    public void Update()
    {
        foreach (var p in this.particles)
        {
            p.X += p.Vx;
            p.Y += p.Vy;
        }
    }

    public void Render()
    {
        /* ... clear video buffer, init something, etc ... */
        foreach (var p in this.particles)
        {
            this.RenderParticle(p);
        }
    }
}

Pretty straightforward. As you can see, each frame we go over entire set of particles and update coordinates of each one according to it's velocity.

Now, if the particle was immutable, there'd be no way to change it's properties, but as been said, we just create a new one with updated values. But hold on a second, doesn't that mean we'd have to replace the original particle object with an updated one in our set? Yes it does. And doesn't that mean that the set now has to be mutable? Looks like we just transfered the mutability from one place to another...

Well, we could refactor to rebuild the entire set of particles each frame. But then, the property that stores the set would have to be mutable... Hmmm. Do you follow?

It appears that at some point you'll have to lose immutability in order to cause effects, and depending on where that point is, your program will or will not have some (arguably) useful properties.

In our case (we're making a video game), we technically can postpone the mutability all the way until we get to drawing on the screen. It's good that the screen is mutable, otherwise we'd have to create a new display each frame :)

Now, let's see how this could be implemented in F#:

type Particle = {
    X : double
    Y : double
    Vx : double
    Vy : double
}

type Model = { Particles : Particle seq }

module Game =
    let update model =
        let updateParticle p =
            { p with X = p.X + p.Vx; Y = p.Y + p.Vy }

        { model with
            Particles = seq {
                for p in model.Particles do
                    yield updateParticle p } }

    let render model =
        for p in model.Particles do
            Graphics.drawParticle p

Note that in order to preserve immutability, we had to make the Update method a function that takes a model object and returns it updated. The Render now also takes the model object as an argument, as we don't keep it in memory anymore because it is immutable.

Hope this helps!

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top